Compare commits
No commits in common. "main" and "lsp" have entirely different histories.
|
|
@ -7,7 +7,7 @@
|
||||||
<link href="/src/app/index.css" rel="stylesheet" />
|
<link href="/src/app/index.css" rel="stylesheet" />
|
||||||
<link rel="stylesheet" href="node_modules/@xterm/xterm/css/xterm.css" />
|
<link rel="stylesheet" href="node_modules/@xterm/xterm/css/xterm.css" />
|
||||||
</head>
|
</head>
|
||||||
<body class="dark:scheme-dark dark:bg-neutral-800 dark:text-gray-100">
|
<body>
|
||||||
<script type="module" src="/src/app/renderer.ts"></script>
|
<script type="module" src="/src/app/renderer.ts"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,12 @@
|
||||||
{
|
{
|
||||||
"name": "miller",
|
"name": "miller",
|
||||||
"version": "0.2.4",
|
"version": "0.2.1",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "miller",
|
"name": "miller",
|
||||||
"version": "0.2.4",
|
"version": "0.2.1",
|
||||||
"license": "GPL-3.0-or-later",
|
"license": "GPL-3.0-or-later",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"chokidar": "^5.0.0",
|
"chokidar": "^5.0.0",
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"name": "miller",
|
"name": "miller",
|
||||||
"productName": "miller",
|
"productName": "miller",
|
||||||
"version": "0.2.4",
|
"version": "0.2.1",
|
||||||
"description": "Column-based code editor",
|
"description": "Column-based code editor",
|
||||||
"main": ".vite/build/main.js",
|
"main": ".vite/build/main.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|
|
||||||
|
|
@ -17,8 +17,8 @@ export abstract class Displayable {
|
||||||
private shortcuts = new Map<string, KeyHandler>();
|
private shortcuts = new Map<string, KeyHandler>();
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
// Attempt to install handlers shortly after construction.
|
// Attempt to install handlers shortly after construction. If `dom` is not
|
||||||
// If `dom` is not available yet, retry a few times.
|
// available yet, retry a few times.
|
||||||
setTimeout(() => this.installHandlers(0), 0);
|
setTimeout(() => this.installHandlers(0), 0);
|
||||||
|
|
||||||
// Add general shortcuts
|
// Add general shortcuts
|
||||||
|
|
@ -47,7 +47,6 @@ export abstract class Displayable {
|
||||||
changeWidth(increment: number) {
|
changeWidth(increment: number) {
|
||||||
const w = parseInt(window.getComputedStyle(this.dom).width, 10);
|
const w = parseInt(window.getComputedStyle(this.dom).width, 10);
|
||||||
this.dom.style.width = w + increment + "px";
|
this.dom.style.width = w + increment + "px";
|
||||||
this.dom.scrollIntoView();
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -32,7 +32,7 @@ import {
|
||||||
import { languages } from "@codemirror/language-data";
|
import { languages } from "@codemirror/language-data";
|
||||||
import { autocompletion, closeBrackets } from "@codemirror/autocomplete";
|
import { autocompletion, closeBrackets } from "@codemirror/autocomplete";
|
||||||
import { highlightSelectionMatches, searchKeymap } from "@codemirror/search";
|
import { highlightSelectionMatches, searchKeymap } from "@codemirror/search";
|
||||||
import { lintGutter, lintKeymap } from "@codemirror/lint";
|
import { lintKeymap } from "@codemirror/lint";
|
||||||
import van from "vanjs-core";
|
import van from "vanjs-core";
|
||||||
import { Displayable } from "./displayable";
|
import { Displayable } from "./displayable";
|
||||||
import { createLspExtension } from "./lsp";
|
import { createLspExtension } from "./lsp";
|
||||||
|
|
@ -41,9 +41,9 @@ import { OpenFile } from "./filestate";
|
||||||
import {
|
import {
|
||||||
findReferencesKeymap,
|
findReferencesKeymap,
|
||||||
formatKeymap,
|
formatKeymap,
|
||||||
|
jumpToDefinitionKeymap,
|
||||||
renameKeymap,
|
renameKeymap,
|
||||||
} from "@codemirror/lsp-client";
|
} from "@codemirror/lsp-client";
|
||||||
import { jumpToDefinitionKeymap } from "./lsp/definition";
|
|
||||||
|
|
||||||
const fixedHeightEditor = EditorView.theme({
|
const fixedHeightEditor = EditorView.theme({
|
||||||
"&": {
|
"&": {
|
||||||
|
|
@ -146,7 +146,6 @@ export class Editor extends Displayable {
|
||||||
lineNumbers(),
|
lineNumbers(),
|
||||||
highlightSpecialChars(),
|
highlightSpecialChars(),
|
||||||
foldGutter(),
|
foldGutter(),
|
||||||
lintGutter(),
|
|
||||||
drawSelection(),
|
drawSelection(),
|
||||||
dropCursor(),
|
dropCursor(),
|
||||||
EditorState.allowMultipleSelections.of(true),
|
EditorState.allowMultipleSelections.of(true),
|
||||||
|
|
|
||||||
|
|
@ -61,11 +61,9 @@ const EditorWrapper = (
|
||||||
});
|
});
|
||||||
|
|
||||||
return v.div(
|
return v.div(
|
||||||
{ class: "flex flex-col group" },
|
{ class: "flex flex-col" },
|
||||||
v.div(
|
v.div(
|
||||||
{
|
{ class: "flex" },
|
||||||
class: "flex group-focus-within:bg-blue-300 dark:group-focus-within:bg-blue-900",
|
|
||||||
},
|
|
||||||
v.span({ class: "mx-1 flex-1" }, () => editor.val.title()),
|
v.span({ class: "mx-1 flex-1" }, () => editor.val.title()),
|
||||||
u.InlineButton(() => editor.val.close(), "Close", "❌"),
|
u.InlineButton(() => editor.val.close(), "Close", "❌"),
|
||||||
),
|
),
|
||||||
|
|
@ -77,11 +75,10 @@ const EditorWrapper = (
|
||||||
const editors: Displayable[][] = vanX.reactive([[]]);
|
const editors: Displayable[][] = vanX.reactive([[]]);
|
||||||
const currentTab = van.state(0);
|
const currentTab = van.state(0);
|
||||||
|
|
||||||
export function addEditor(file: OpenFile): Editor {
|
export function addEditor(file: OpenFile) {
|
||||||
const editor = file.createEditor();
|
const editor = file.createEditor();
|
||||||
editors[currentTab.val].push(vanX.noreactive(editor));
|
editors[currentTab.val].push(vanX.noreactive(editor));
|
||||||
editor.focus();
|
editor.focus();
|
||||||
return editor;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function addTab() {
|
export function addTab() {
|
||||||
|
|
@ -101,7 +98,7 @@ const TabHeader = (tab: State<Editor[]>, del: () => void, k: number) =>
|
||||||
v.div(
|
v.div(
|
||||||
{
|
{
|
||||||
class: () =>
|
class: () =>
|
||||||
`flex-auto flex ${currentTab.val === k ? "bg-green-500 dark:bg-green-700" : ""}`,
|
`flex-auto flex ${currentTab.val === k ? "bg-green-500" : ""}`,
|
||||||
onclick: () => (currentTab.val = k),
|
onclick: () => (currentTab.val = k),
|
||||||
},
|
},
|
||||||
v.span({ class: "mx-1 flex-1" }, "Tab " + k),
|
v.span({ class: "mx-1 flex-1" }, "Tab " + k),
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,6 @@ import {
|
||||||
ChangeSet,
|
ChangeSet,
|
||||||
} from "@codemirror/state";
|
} from "@codemirror/state";
|
||||||
import { history } from "@codemirror/commands";
|
import { history } from "@codemirror/commands";
|
||||||
import { Diagnostic, setDiagnostics } from "@codemirror/lint";
|
|
||||||
import { Editor } from "./editor";
|
import { Editor } from "./editor";
|
||||||
import van, { State } from "vanjs-core";
|
import van, { State } from "vanjs-core";
|
||||||
import { WorkspaceFile } from "@codemirror/lsp-client";
|
import { WorkspaceFile } from "@codemirror/lsp-client";
|
||||||
|
|
@ -105,11 +104,6 @@ export class OpenFile implements WorkspaceFile {
|
||||||
createEditor(): Editor {
|
createEditor(): Editor {
|
||||||
const editor = new Editor(this);
|
const editor = new Editor(this);
|
||||||
this.editors.push(editor);
|
this.editors.push(editor);
|
||||||
editor.dispatch(
|
|
||||||
editor.view.state.update(
|
|
||||||
setDiagnostics(editor.view.state, this.diagnostics || []),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
return editor;
|
return editor;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -167,7 +161,10 @@ export class OpenFile implements WorkspaceFile {
|
||||||
this.rootState.val = transaction.state;
|
this.rootState.val = transaction.state;
|
||||||
|
|
||||||
if (transaction.changes && !transaction.changes.empty) {
|
if (transaction.changes && !transaction.changes.empty) {
|
||||||
this.changeSet = this.changes.compose(transaction.changes);
|
if (this.changes === undefined) {
|
||||||
|
this.changes = ChangeSet.empty(this.rootState.val.doc.length);
|
||||||
|
}
|
||||||
|
this.changes = this.changes.compose(transaction.changes);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (origin) {
|
if (origin) {
|
||||||
|
|
@ -205,19 +202,8 @@ export class OpenFile implements WorkspaceFile {
|
||||||
get languageId(): string {
|
get languageId(): string {
|
||||||
return inferLanguageFromPath(this.filePath.val || "") || "";
|
return inferLanguageFromPath(this.filePath.val || "") || "";
|
||||||
}
|
}
|
||||||
|
|
||||||
doc: Text;
|
doc: Text;
|
||||||
private changeSet: ChangeSet;
|
changes: ChangeSet;
|
||||||
get changes(): ChangeSet {
|
|
||||||
if (!this.changeSet) {
|
|
||||||
this.clearChanges();
|
|
||||||
}
|
|
||||||
return this.changeSet;
|
|
||||||
}
|
|
||||||
clearChanges(): void {
|
|
||||||
this.changeSet = ChangeSet.empty(this.rootState.val.doc.length);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Return an EditorView to be used by the LSP Workspace for position mapping.
|
// Return an EditorView to be used by the LSP Workspace for position mapping.
|
||||||
// If `main` is provided and belongs to this open file, return it. Otherwise
|
// If `main` is provided and belongs to this open file, return it. Otherwise
|
||||||
// return the first available editor view, or null if none exist.
|
// return the first available editor view, or null if none exist.
|
||||||
|
|
@ -229,14 +215,4 @@ export class OpenFile implements WorkspaceFile {
|
||||||
if (this.editors.length > 0) return this.editors[0].view;
|
if (this.editors.length > 0) return this.editors[0].view;
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private diagnostics: Diagnostic[];
|
|
||||||
setDiagnostics(diagnostics: Diagnostic[]) {
|
|
||||||
this.diagnostics = diagnostics;
|
|
||||||
for (const editor of this.editors) {
|
|
||||||
editor.view.dispatch(
|
|
||||||
setDiagnostics(editor.view.state, diagnostics),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,12 +8,6 @@ import * as u from "./utils";
|
||||||
|
|
||||||
const folderTreeState = van.state<FolderTree | null>(null);
|
const folderTreeState = van.state<FolderTree | null>(null);
|
||||||
|
|
||||||
van.derive(() => {
|
|
||||||
if (folderTreeState.val) {
|
|
||||||
document.title = folderTreeState.val.path + " - Miller code editor";
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
async function openFolder() {
|
async function openFolder() {
|
||||||
const folderTree = await window.electronAPI.openFolder().catch(alert);
|
const folderTree = await window.electronAPI.openFolder().catch(alert);
|
||||||
if (!folderTree) return;
|
if (!folderTree) return;
|
||||||
|
|
@ -96,7 +90,7 @@ const FsItemView = (tree: FolderTree): HTMLElement => {
|
||||||
if (tree.type === "file")
|
if (tree.type === "file")
|
||||||
return v.p(
|
return v.p(
|
||||||
{
|
{
|
||||||
class: "cursor-pointer hover:bg-gray-100 dark:hover:bg-gray-700",
|
class: "cursor-pointer hover:bg-gray-100",
|
||||||
onclick: async () =>
|
onclick: async () =>
|
||||||
addEditor(await OpenFile.openFile(tree.path)),
|
addEditor(await OpenFile.openFile(tree.path)),
|
||||||
},
|
},
|
||||||
|
|
@ -106,22 +100,20 @@ const FsItemView = (tree: FolderTree): HTMLElement => {
|
||||||
const isOpen = van.state(false);
|
const isOpen = van.state(false);
|
||||||
const children = () =>
|
const children = () =>
|
||||||
isOpen.val
|
isOpen.val
|
||||||
? v.ul({ class: "pl-4" }, tree.children?.map(FsItemView))
|
? v.ul({}, tree.children?.map(FsItemView))
|
||||||
: v.div({ ariaBusy: true });
|
: v.div({ ariaBusy: true });
|
||||||
const folder = v.details(
|
const folder = v.details(
|
||||||
{
|
{
|
||||||
class: "flex-auto inline",
|
class: "flex-auto inline",
|
||||||
ontoggle: () => (isOpen.val = folder.open),
|
ontoggle: () => (isOpen.val = folder.open),
|
||||||
},
|
},
|
||||||
v.summary(
|
v.summary(tree.name),
|
||||||
{
|
|
||||||
class: "cursor-pointer flex hover:bg-gray-100 dark:hover:bg-gray-700",
|
|
||||||
},
|
|
||||||
v.span(() => (isOpen.val ? "📂" : "📁")),
|
|
||||||
tree.name,
|
|
||||||
),
|
|
||||||
children,
|
children,
|
||||||
);
|
);
|
||||||
|
|
||||||
return folder;
|
return v.div(
|
||||||
|
{ class: "cursor-pointer flex hover:bg-gray-100" },
|
||||||
|
v.span(() => (isOpen.val ? "📂" : "📁")),
|
||||||
|
folder,
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,19 +1,16 @@
|
||||||
import * as lsp from "vscode-languageserver-protocol";
|
// Minimal LSP integration helper for the editor.
|
||||||
import { Extension, TransactionSpec } from "@codemirror/state";
|
// Keeps all LSP-specific logic in one place so it's easy to review.
|
||||||
|
|
||||||
|
import { Extension, ChangeSet, TransactionSpec } from "@codemirror/state";
|
||||||
import { EditorView } from "@codemirror/view";
|
import { EditorView } from "@codemirror/view";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
LSPClient,
|
LSPClient,
|
||||||
LSPPlugin,
|
languageServerExtensions,
|
||||||
Workspace,
|
Workspace,
|
||||||
hoverTooltips,
|
|
||||||
signatureHelp,
|
|
||||||
WorkspaceMapping
|
|
||||||
} from "@codemirror/lsp-client";
|
} from "@codemirror/lsp-client";
|
||||||
|
|
||||||
import { serverCompletion } from "./lsp/completion";
|
|
||||||
import { OpenFile } from "./filestate";
|
import { OpenFile } from "./filestate";
|
||||||
import { serverDiagnostics } from "./lsp/diagnostics";
|
|
||||||
|
|
||||||
// Create a very small MessagePort-based transport implementation
|
// Create a very small MessagePort-based transport implementation
|
||||||
// compatible with @codemirror/lsp-client's expected Transport interface.
|
// compatible with @codemirror/lsp-client's expected Transport interface.
|
||||||
|
|
@ -78,23 +75,6 @@ export function inferLanguageFromPath(
|
||||||
if (ext === "ts" || ext === "tsx" || ext === "js" || ext === "jsx")
|
if (ext === "ts" || ext === "tsx" || ext === "js" || ext === "jsx")
|
||||||
return "typescript";
|
return "typescript";
|
||||||
if (ext === "py") return "python";
|
if (ext === "py") return "python";
|
||||||
if (ext === "c" || ext === "h" || ext === "cpp" || ext === "hpp")
|
|
||||||
return "cpp";
|
|
||||||
if (ext === "java") return "java";
|
|
||||||
if (ext === "go") return "go";
|
|
||||||
if (ext === "rs") return "rust";
|
|
||||||
if (ext === "php") return "php";
|
|
||||||
if (ext === "rb") return "ruby";
|
|
||||||
if (ext === "cs") return "csharp";
|
|
||||||
if (ext === "html" || ext === "htm") return "html";
|
|
||||||
if (ext === "css" || ext === "scss" || ext === "less") return "css";
|
|
||||||
if (ext === "json") return "json";
|
|
||||||
if (ext === "xml") return "xml";
|
|
||||||
if (ext === "yaml" || ext === "yml") return "yaml";
|
|
||||||
if (ext === "md") return "markdown";
|
|
||||||
if (ext === "lua") return "lua";
|
|
||||||
if (ext === "sh" || ext === "bash") return "shellscript";
|
|
||||||
if (ext === "hs") return "haskell";
|
|
||||||
// add more mappings as needed
|
// add more mappings as needed
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
@ -125,19 +105,13 @@ class OpenFileWorkspace extends Workspace {
|
||||||
result.push({ file, prevDoc, changes });
|
result.push({ file, prevDoc, changes });
|
||||||
file.doc = file.rootState.val.doc;
|
file.doc = file.rootState.val.doc;
|
||||||
file.version = this.nextFileVersion(file.uri);
|
file.version = this.nextFileVersion(file.uri);
|
||||||
file.clearChanges();
|
file.changes = ChangeSet.empty(file.doc.length);
|
||||||
}
|
|
||||||
for (const e of file.editors) {
|
|
||||||
const plugin = LSPPlugin.get(e.view);
|
|
||||||
if (plugin) {
|
|
||||||
plugin.clear();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
openFile(uri: string, _languageId: string, view: EditorView) {
|
openFile(uri: string, languageId: string, view: EditorView) {
|
||||||
console.log("LSP: attempting to open file", uri);
|
console.log("LSP: attempting to open file", uri);
|
||||||
if (this.getFile(uri)) return;
|
if (this.getFile(uri)) return;
|
||||||
// Try to map to an existing OpenFile instance, prefer using its doc
|
// Try to map to an existing OpenFile instance, prefer using its doc
|
||||||
|
|
@ -162,7 +136,7 @@ class OpenFileWorkspace extends Workspace {
|
||||||
file.dispatch(update);
|
file.dispatch(update);
|
||||||
}
|
}
|
||||||
|
|
||||||
closeFile(uri: string, _view: EditorView) {
|
closeFile(uri: string, view: EditorView) {
|
||||||
const path = uri.replace(/^file:\/\//, "");
|
const path = uri.replace(/^file:\/\//, "");
|
||||||
const of = OpenFile.findOpenFile(path);
|
const of = OpenFile.findOpenFile(path);
|
||||||
// If OpenFile exists and still has editors, defer closing
|
// If OpenFile exists and still has editors, defer closing
|
||||||
|
|
@ -174,35 +148,6 @@ class OpenFileWorkspace extends Workspace {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function applyTextEdits(mapping: WorkspaceMapping, workspace: Workspace, uri: string, edits: lsp.TextEdit[], userEvent: string) {
|
|
||||||
const file = workspace.getFile(uri);
|
|
||||||
if(!file) return;
|
|
||||||
workspace.updateFile(uri, {
|
|
||||||
changes: edits.map(change => ({
|
|
||||||
from: mapping.mapPosition(uri, change.range.start),
|
|
||||||
to: mapping.mapPosition(uri, change.range.end),
|
|
||||||
insert: change.newText,
|
|
||||||
})),
|
|
||||||
userEvent,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
export function applyWorkspaceEdit(mapping: WorkspaceMapping, workspace: Workspace, edit: lsp.WorkspaceEdit, userEvent: string) {
|
|
||||||
for (const uri in edit.changes) {
|
|
||||||
const lspChanges = edit.changes[uri];
|
|
||||||
if (!lspChanges.length) continue;
|
|
||||||
applyTextEdits(mapping, workspace, uri, lspChanges, userEvent);
|
|
||||||
}
|
|
||||||
for (const change of edit.documentChanges) {
|
|
||||||
if (Object.hasOwn(change, "kind")) {
|
|
||||||
const action = change as (lsp.CreateFile | lsp.RenameFile | lsp.DeleteFile);
|
|
||||||
console.warn("Unsupported edit type!", action.kind);
|
|
||||||
}
|
|
||||||
const lspChanges = change as lsp.TextDocumentEdit;
|
|
||||||
applyTextEdits(mapping, workspace, lspChanges.textDocument.uri, lspChanges.edits, userEvent);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Public helper: attempt to create an LSP extension for `filePath`.
|
// Public helper: attempt to create an LSP extension for `filePath`.
|
||||||
// Returns an empty array (no-op extension) on failure so callers can safely
|
// Returns an empty array (no-op extension) on failure so callers can safely
|
||||||
// reconfigure their compartments with the returned value.
|
// reconfigure their compartments with the returned value.
|
||||||
|
|
@ -305,12 +250,7 @@ export async function createLspExtension(
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const client = new LSPClient({
|
const client = new LSPClient({
|
||||||
extensions: [
|
extensions: languageServerExtensions(),
|
||||||
serverDiagnostics(),
|
|
||||||
serverCompletion(),
|
|
||||||
hoverTooltips(),
|
|
||||||
signatureHelp(),
|
|
||||||
],
|
|
||||||
rootUri: rootUri,
|
rootUri: rootUri,
|
||||||
workspace: (c) => new OpenFileWorkspace(c),
|
workspace: (c) => new OpenFileWorkspace(c),
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,88 +0,0 @@
|
||||||
import * as lsp from "vscode-languageserver-protocol";
|
|
||||||
import { StateEffect, StateField } from "@codemirror/state";
|
|
||||||
import { GutterMarker, ViewPlugin, ViewUpdate } from "@codemirror/view";
|
|
||||||
import { LSPPlugin, LSPClientExtension } from "@codemirror/lsp-client";
|
|
||||||
|
|
||||||
type GutterCodeAction = {
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
const codeActionGutterMarker = new class extends GutterMarker {
|
|
||||||
toDOM() { return document.createTextNode("💡"); }
|
|
||||||
};
|
|
||||||
|
|
||||||
const codeActionGutterEffect = StateEffect.define<lsp.CodeAction[]>();
|
|
||||||
|
|
||||||
const codeActionGutterState = StateField.define<lsp.CodeAction[]>({
|
|
||||||
create() { return []; },
|
|
||||||
update(updateCodeActions, tr) {
|
|
||||||
for (let e of tr.effects) {
|
|
||||||
if (e.is(codeActionGutterEffect)) {
|
|
||||||
return e.value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return updateCodeActions;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const syncCodeAction = ViewPlugin.fromClass(
|
|
||||||
class {
|
|
||||||
pending: any | null = null;
|
|
||||||
update(update: ViewUpdate) {
|
|
||||||
if (update.docChanged) {
|
|
||||||
if (this.pending != null) clearTimeout(this.pending);
|
|
||||||
this.pending = setTimeout(() => {
|
|
||||||
this.pending = null;
|
|
||||||
const plugin = LSPPlugin.get(update.view);
|
|
||||||
if (plugin) {
|
|
||||||
plugin.client.sync();
|
|
||||||
updateCodeActions(plugin);
|
|
||||||
}
|
|
||||||
}, 500);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
destroy() {
|
|
||||||
if (this.pending != null) clearTimeout(this.pending);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
function updateCodeActions(plugin: LSPPlugin) {
|
|
||||||
const hasCap = plugin.client.serverCapabilities ? !!plugin.client.serverCapabilities["codeActionProvider"] : null;
|
|
||||||
if (hasCap === false) return Promise.resolve(null);
|
|
||||||
const params: lsp.CodeActionParams = {
|
|
||||||
textDocument: { uri: plugin.uri },
|
|
||||||
range: {
|
|
||||||
start: plugin.toPosition(0),
|
|
||||||
end: plugin.toPosition(plugin.syncedDoc.length),
|
|
||||||
},
|
|
||||||
context: {
|
|
||||||
diagnostics: [], // TODO fix
|
|
||||||
triggerKind: lsp.CodeActionTriggerKind.Automatic,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
plugin.client.request<
|
|
||||||
lsp.CodeActionParams,
|
|
||||||
(lsp.Command | lsp.CodeAction)[]
|
|
||||||
>(
|
|
||||||
"textDocument/codeAction",
|
|
||||||
params
|
|
||||||
).then((actions) => {
|
|
||||||
const file = plugin.client.workspace.getFile(plugin.uri) as OpenFile;
|
|
||||||
if (!file) return false;
|
|
||||||
if ()
|
|
||||||
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export function codeAction(config: {}): LSPClientExtension {
|
|
||||||
return {
|
|
||||||
clientCapabilities: {
|
|
||||||
codeAction: {
|
|
||||||
dataSupport: true,
|
|
||||||
resolveSupport: ["edit"],
|
|
||||||
}
|
|
||||||
},
|
|
||||||
editorExtension: syncCodeAction,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,183 +0,0 @@
|
||||||
import type * as lsp from "vscode-languageserver-protocol"
|
|
||||||
import { EditorState, Extension, Facet } from "@codemirror/state"
|
|
||||||
import { CompletionSource, Completion, CompletionContext, snippet, autocompletion } from "@codemirror/autocomplete"
|
|
||||||
import { LSPPlugin, LSPClientExtension } from "@codemirror/lsp-client"
|
|
||||||
|
|
||||||
/// Register the [language server completion
|
|
||||||
/// source](#lsp-client.serverCompletionSource) as an autocompletion
|
|
||||||
/// source.
|
|
||||||
export function serverCompletion(config: {
|
|
||||||
/// By default, the completion source that asks the language server
|
|
||||||
/// for completions is added as a regular source, in addition to any
|
|
||||||
/// other sources. Set this to true to make it replace all
|
|
||||||
/// completion sources.
|
|
||||||
override?: boolean
|
|
||||||
/// Set a custom
|
|
||||||
/// [`validFor`](#autocomplete.CompletionResult.validFor) expression
|
|
||||||
/// to use in the completion results. By default, the library uses an
|
|
||||||
/// expression that accepts word characters, optionally prefixed by
|
|
||||||
/// any non-word prefixes found in the results.
|
|
||||||
validFor?: RegExp
|
|
||||||
} = {}): LSPClientExtension {
|
|
||||||
let result: Extension[]
|
|
||||||
if (config.override) {
|
|
||||||
result = [autocompletion({ override: [serverCompletionSource] })]
|
|
||||||
} else {
|
|
||||||
let data = [{ autocomplete: serverCompletionSource }]
|
|
||||||
result = [autocompletion(), EditorState.languageData.of(() => data)]
|
|
||||||
}
|
|
||||||
if (config.validFor) result.push(completionConfig.of({ validFor: config.validFor }))
|
|
||||||
return {
|
|
||||||
clientCapabilities: {
|
|
||||||
textDocument: {
|
|
||||||
completion: {
|
|
||||||
completionItem: {
|
|
||||||
snippetSupport: true,
|
|
||||||
// documentationFormat: ["markdown", "plaintext"],
|
|
||||||
resolveSupport: ["documentation"],
|
|
||||||
labelDetailsSupport: true,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
editorExtension: result,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const completionConfig = Facet.define<{ validFor: RegExp }, { validFor: RegExp | null }>({
|
|
||||||
combine: results => results.length ? results[0] : { validFor: null }
|
|
||||||
})
|
|
||||||
|
|
||||||
function getCompletions(plugin: LSPPlugin, pos: number, context: lsp.CompletionContext, abort?: CompletionContext) {
|
|
||||||
const hasCapability = plugin.client.serverCapabilities ? !!plugin.client.serverCapabilities["completionProvider"] : null;
|
|
||||||
if (hasCapability === false) return Promise.resolve(null)
|
|
||||||
plugin.client.sync()
|
|
||||||
let params: lsp.CompletionParams = {
|
|
||||||
position: plugin.toPosition(pos),
|
|
||||||
textDocument: { uri: plugin.uri },
|
|
||||||
context
|
|
||||||
}
|
|
||||||
if (abort) abort.addEventListener("abort", () => plugin.client.cancelRequest(params))
|
|
||||||
return plugin.client.request<lsp.CompletionParams, lsp.CompletionItem[] | lsp.CompletionList | null>(
|
|
||||||
"textDocument/completion", params)
|
|
||||||
}
|
|
||||||
|
|
||||||
async function resolveDocs(plugin: LSPPlugin, item: lsp.CompletionItem, abort?: CompletionContext): Promise<lsp.CompletionItem> {
|
|
||||||
const hasCapability = plugin.client.serverCapabilities ? !!plugin.client.serverCapabilities["completionProvider"]["resolveProvider"] : null;
|
|
||||||
if (hasCapability === false) {
|
|
||||||
console.warn("No resolving support but also no docs given!");
|
|
||||||
return Promise.resolve(null);
|
|
||||||
}
|
|
||||||
if (abort) abort.addEventListener("abort", () => plugin.client.cancelRequest(item));
|
|
||||||
return await plugin.client.request("completionItem/resolve", item);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Look for non-alphanumeric prefixes in the completions, and return a
|
|
||||||
// regexp that matches them, to use in validFor
|
|
||||||
function prefixRegexp(items: readonly lsp.CompletionItem[]) {
|
|
||||||
let step = Math.ceil(items.length / 50), prefixes: string[] = []
|
|
||||||
for (let i = 0; i < items.length; i += step) {
|
|
||||||
let item = items[i], text = item.textEdit?.newText || item.textEditText || item.insertText || item.label
|
|
||||||
if (!/^\w/.test(text)) {
|
|
||||||
let prefix = /^[^\w]*/.exec(text)![0]
|
|
||||||
if (prefixes.indexOf(prefix) < 0) prefixes.push(prefix)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!prefixes.length) return /^\w*$/
|
|
||||||
return new RegExp("^(?:" + prefixes.map((RegExp as any).escape || (s => s.replace(/[^\w\s]/g, "\\$&"))).join("|") + ")?\\w*$")
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A completion source that requests completions from a language
|
|
||||||
/// server.
|
|
||||||
export const serverCompletionSource: CompletionSource = context => {
|
|
||||||
const plugin = context.view && LSPPlugin.get(context.view)
|
|
||||||
if (!plugin) return null
|
|
||||||
let triggerChar = ""
|
|
||||||
if (!context.explicit) {
|
|
||||||
triggerChar = context.view.state.sliceDoc(context.pos - 1, context.pos)
|
|
||||||
let triggers = plugin.client.serverCapabilities?.completionProvider?.triggerCharacters
|
|
||||||
if (!/[a-zA-Z_]/.test(triggerChar) && !(triggers && triggers.indexOf(triggerChar) > -1)) return null
|
|
||||||
}
|
|
||||||
return getCompletions(plugin, context.pos, {
|
|
||||||
triggerCharacter: triggerChar,
|
|
||||||
triggerKind: context.explicit ? 1 /* Invoked */ : 2 /* TriggerCharacter */
|
|
||||||
}, context).then(result => {
|
|
||||||
if (!result) return null
|
|
||||||
if (Array.isArray(result)) result = { items: result } as lsp.CompletionList
|
|
||||||
let { from, to } = completionResultRange(context, result)
|
|
||||||
let defaultCommitChars = result.itemDefaults?.commitCharacters
|
|
||||||
let config = context.state.facet(completionConfig)
|
|
||||||
|
|
||||||
return {
|
|
||||||
from, to,
|
|
||||||
options: result.items.map((item: lsp.CompletionItem) => {
|
|
||||||
let text = item.textEdit?.newText || item.textEditText || item.insertText || item.label
|
|
||||||
let option: Completion = {
|
|
||||||
label: text,
|
|
||||||
type: item.kind && kindToType[item.kind],
|
|
||||||
}
|
|
||||||
if (item.commitCharacters && item.commitCharacters != defaultCommitChars)
|
|
||||||
option.commitCharacters = item.commitCharacters
|
|
||||||
if (item.detail) option.detail = item.detail; else option.detail = "No details available"
|
|
||||||
if (item.sortText) option.sortText = item.sortText
|
|
||||||
if (item.insertTextFormat == 2 /* Snippet */) {
|
|
||||||
option.apply = (view, c, from, to) => snippet(text)(view, c, from, to)
|
|
||||||
option.label = item.label
|
|
||||||
}
|
|
||||||
option.info = item.documentation ? () => renderDocInfo(plugin, item.documentation!) : async () => {
|
|
||||||
const newItem = await resolveDocs(plugin, item);
|
|
||||||
return newItem.documentation ? renderDocInfo(plugin, newItem.documentation!) : null;
|
|
||||||
}
|
|
||||||
// if (item.documentation) option.info = () => renderDocInfo(plugin, item.documentation!);
|
|
||||||
return option
|
|
||||||
}),
|
|
||||||
commitCharacters: defaultCommitChars,
|
|
||||||
validFor: config.validFor ?? prefixRegexp(result.items),
|
|
||||||
map: (result, changes) => ({ ...result, from: changes.mapPos(result.from) }),
|
|
||||||
}
|
|
||||||
}, err => {
|
|
||||||
if ("code" in err && (err as lsp.ResponseError).code == -32800 /* RequestCancelled */)
|
|
||||||
return null
|
|
||||||
throw err
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
function completionResultRange(cx: CompletionContext, result: lsp.CompletionList): { from: number, to: number } {
|
|
||||||
if (!result.items.length) return { from: cx.pos, to: cx.pos }
|
|
||||||
let defaultRange = result.itemDefaults?.editRange, item0 = result.items[0]
|
|
||||||
let range = defaultRange ? ("insert" in defaultRange ? defaultRange.insert : defaultRange)
|
|
||||||
: item0.textEdit ? ("range" in item0.textEdit ? item0.textEdit.range : item0.textEdit.insert)
|
|
||||||
: null
|
|
||||||
if (!range) return cx.state.wordAt(cx.pos) || { from: cx.pos, to: cx.pos }
|
|
||||||
let line = cx.state.doc.lineAt(cx.pos)
|
|
||||||
return { from: line.from + range.start.character, to: line.from + range.end.character }
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderDocInfo(plugin: LSPPlugin, doc: string | lsp.MarkupContent) {
|
|
||||||
let elt = document.createElement("div")
|
|
||||||
elt.className = "cm-lsp-documentation cm-lsp-completion-documentation"
|
|
||||||
elt.innerHTML = plugin.docToHTML(doc)
|
|
||||||
return elt
|
|
||||||
}
|
|
||||||
|
|
||||||
const kindToType: { [kind: number]: string } = {
|
|
||||||
1: "text", // Text
|
|
||||||
2: "method", // Method
|
|
||||||
3: "function", // Function
|
|
||||||
4: "class", // Constructor
|
|
||||||
5: "property", // Field
|
|
||||||
6: "variable", // Variable
|
|
||||||
7: "class", // Class
|
|
||||||
8: "interface", // Interface
|
|
||||||
9: "namespace", // Module
|
|
||||||
10: "property", // Property
|
|
||||||
11: "keyword", // Unit
|
|
||||||
12: "constant", // Value
|
|
||||||
13: "constant", // Enum
|
|
||||||
14: "keyword", // Keyword
|
|
||||||
16: "constant", // Color
|
|
||||||
20: "constant", // EnumMember
|
|
||||||
21: "constant", // Constant
|
|
||||||
22: "class", // Struct
|
|
||||||
25: "type" // TypeParameter
|
|
||||||
}
|
|
||||||
|
|
@ -1,81 +0,0 @@
|
||||||
import type * as lsp from "vscode-languageserver-protocol"
|
|
||||||
import { EditorView, Command, KeyBinding } from "@codemirror/view"
|
|
||||||
import { LSPPlugin } from "@codemirror/lsp-client";
|
|
||||||
|
|
||||||
import { addEditor } from "../editorgrid";
|
|
||||||
import { OpenFile } from "../filestate";
|
|
||||||
|
|
||||||
function getDefinition(plugin: LSPPlugin, pos: number) {
|
|
||||||
return plugin.client.request<lsp.DefinitionParams, lsp.Location | lsp.Location[] | null>("textDocument/definition", {
|
|
||||||
textDocument: { uri: plugin.uri },
|
|
||||||
position: plugin.toPosition(pos)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
function getDeclaration(plugin: LSPPlugin, pos: number) {
|
|
||||||
return plugin.client.request<lsp.DeclarationParams, lsp.Location | lsp.Location[] | null>("textDocument/declaration", {
|
|
||||||
textDocument: { uri: plugin.uri },
|
|
||||||
position: plugin.toPosition(pos)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
function getTypeDefinition(plugin: LSPPlugin, pos: number) {
|
|
||||||
return plugin.client.request<lsp.TypeDefinitionParams, lsp.Location | lsp.Location[] | null>("textDocument/typeDefinition", {
|
|
||||||
textDocument: { uri: plugin.uri },
|
|
||||||
position: plugin.toPosition(pos)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
function getImplementation(plugin: LSPPlugin, pos: number) {
|
|
||||||
return plugin.client.request<lsp.ImplementationParams, lsp.Location | lsp.Location[] | null>("textDocument/implementation", {
|
|
||||||
textDocument: { uri: plugin.uri },
|
|
||||||
position: plugin.toPosition(pos)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
function jumpToOrigin(view: EditorView, type: { get: typeof getDefinition, capability: keyof lsp.ServerCapabilities }): boolean {
|
|
||||||
const plugin = LSPPlugin.get(view);
|
|
||||||
const hasCapability = plugin.client.serverCapabilities ? !!plugin.client.serverCapabilities[type.capability] : null;
|
|
||||||
if (!plugin || !hasCapability) return false
|
|
||||||
plugin.client.sync()
|
|
||||||
plugin.client.withMapping(mapping => type.get(plugin, view.state.selection.main.head).then(async response => {
|
|
||||||
if (!response) return
|
|
||||||
let loc = Array.isArray(response) ? response[0] : response;
|
|
||||||
const path = new URL(loc.uri).pathname;
|
|
||||||
const target = addEditor(await OpenFile.openFile(path));
|
|
||||||
const pos = mapping.getMapping(loc.uri) ? mapping.mapPosition(loc.uri, loc.range.start) : plugin.fromPosition(loc.range.start, target.view.state.doc);
|
|
||||||
target.view.dispatch({selection: {anchor: pos}, scrollIntoView: true, userEvent: "select.definition"});
|
|
||||||
}, error => plugin.reportError("Find definition failed", error)))
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Jump to the definition of the symbol at the cursor. To support
|
|
||||||
/// cross-file jumps, you'll need to implement
|
|
||||||
/// [`Workspace.displayFile`](#lsp-client.Workspace.displayFile).
|
|
||||||
export const jumpToDefinition: Command = view => jumpToOrigin(view, {
|
|
||||||
get: getDefinition,
|
|
||||||
capability: "definitionProvider"
|
|
||||||
})
|
|
||||||
|
|
||||||
/// Jump to the declaration of the symbol at the cursor.
|
|
||||||
export const jumpToDeclaration: Command = view => jumpToOrigin(view, {
|
|
||||||
get: getDeclaration,
|
|
||||||
capability: "declarationProvider"
|
|
||||||
})
|
|
||||||
|
|
||||||
/// Jump to the type definition of the symbol at the cursor.
|
|
||||||
export const jumpToTypeDefinition: Command = view => jumpToOrigin(view, {
|
|
||||||
get: getTypeDefinition,
|
|
||||||
capability: "typeDefinitionProvider"
|
|
||||||
})
|
|
||||||
|
|
||||||
/// Jump to the implementation of the symbol at the cursor.
|
|
||||||
export const jumpToImplementation: Command = view => jumpToOrigin(view, {
|
|
||||||
get: getImplementation,
|
|
||||||
capability: "implementationProvider"
|
|
||||||
})
|
|
||||||
|
|
||||||
/// Binds F12 to [`jumpToDefinition`](#lsp-client.jumpToDefinition).
|
|
||||||
export const jumpToDefinitionKeymap: readonly KeyBinding[] = [
|
|
||||||
{ key: "F12", run: jumpToDefinition, preventDefault: true },
|
|
||||||
]
|
|
||||||
|
|
@ -1,152 +0,0 @@
|
||||||
import * as lsp from "vscode-languageserver-protocol";
|
|
||||||
import { StateField, StateEffect } from "@codemirror/state";
|
|
||||||
import { showPanel, EditorView, ViewPlugin, ViewUpdate } from "@codemirror/view";
|
|
||||||
import { LSPPlugin, LSPClientExtension } from "@codemirror/lsp-client";
|
|
||||||
import { OpenFile } from "../filestate";
|
|
||||||
import { Text } from "@codemirror/state";
|
|
||||||
import { applyWorkspaceEdit } from "../lsp";
|
|
||||||
|
|
||||||
import van from "vanjs-core";
|
|
||||||
const v = van.tags;
|
|
||||||
|
|
||||||
type CodeAction = (lsp.CodeAction | lsp.Command);
|
|
||||||
|
|
||||||
const setCodeActions = StateEffect.define<CodeAction[]>();
|
|
||||||
|
|
||||||
const codeActionState = StateField.define<CodeAction[]>({
|
|
||||||
create: () => null,
|
|
||||||
update(value, tr) {
|
|
||||||
for (let e of tr.effects) {
|
|
||||||
if (e.is(setCodeActions)) value = e.value;
|
|
||||||
}
|
|
||||||
return value;
|
|
||||||
},
|
|
||||||
provide: f => showPanel.from(f, ca => ca ? createCodeActionPanel : null),
|
|
||||||
});
|
|
||||||
|
|
||||||
function createCodeActionPanel(view: EditorView) {
|
|
||||||
const plugin = LSPPlugin.get(view);
|
|
||||||
const actions = view.state.field(codeActionState);
|
|
||||||
const mapping = plugin.client.workspaceMapping();
|
|
||||||
// TODO cleanup mapping when done
|
|
||||||
// TODO use codemirror/view.showDialog instead
|
|
||||||
const list = v.ul(actions.map(a => {
|
|
||||||
if (!Object.hasOwn(a, "edit")) return null;
|
|
||||||
const action = a as lsp.CodeAction;
|
|
||||||
// TODO action resolving
|
|
||||||
const onclick = () => {
|
|
||||||
applyWorkspaceEdit(mapping, plugin.client.workspace, action.edit, "codeaction");
|
|
||||||
view.dispatch({effects: setCodeActions.of(null)});
|
|
||||||
}
|
|
||||||
return v.li(v.a({onclick}, action.title));
|
|
||||||
}));
|
|
||||||
const closeBtn = v.button({
|
|
||||||
class: "absolute top-0 right-5",
|
|
||||||
type: "button",
|
|
||||||
name: "close",
|
|
||||||
"aria-label": view.state.phrase("close"),
|
|
||||||
onclick: () => view.dispatch({effects: setCodeActions.of(null)}),
|
|
||||||
}, "×");
|
|
||||||
const dom = v.div(list, closeBtn);
|
|
||||||
return { top: false, dom };
|
|
||||||
}
|
|
||||||
|
|
||||||
function toSeverity(sev: lsp.DiagnosticSeverity) {
|
|
||||||
return sev == 1
|
|
||||||
? "error"
|
|
||||||
: sev == 2
|
|
||||||
? "warning"
|
|
||||||
: sev == 3
|
|
||||||
? "info"
|
|
||||||
: "hint";
|
|
||||||
}
|
|
||||||
|
|
||||||
const autoSync = ViewPlugin.fromClass(
|
|
||||||
class {
|
|
||||||
pending: any | null = null;
|
|
||||||
update(update: ViewUpdate) {
|
|
||||||
if (update.docChanged) {
|
|
||||||
if (this.pending != null) clearTimeout(this.pending);
|
|
||||||
this.pending = setTimeout(() => {
|
|
||||||
this.pending = null;
|
|
||||||
const plugin = LSPPlugin.get(update.view);
|
|
||||||
if (plugin) plugin.client.sync();
|
|
||||||
}, 500);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
destroy() {
|
|
||||||
if (this.pending != null) clearTimeout(this.pending);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
function fromPosition(doc: Text, pos: lsp.Position): number {
|
|
||||||
const line = doc.line(pos.line + 1);
|
|
||||||
return line.from + pos.character;
|
|
||||||
}
|
|
||||||
|
|
||||||
function fetchCodeActionsForDiagnostic(plugin: LSPPlugin, diag: lsp.Diagnostic): Promise<CodeAction[]> {
|
|
||||||
const hasCap = plugin.client.serverCapabilities ? !!plugin.client.serverCapabilities["codeActionProvider"] : null;
|
|
||||||
if (hasCap === false) return Promise.resolve(null);
|
|
||||||
const params: lsp.CodeActionParams = {
|
|
||||||
textDocument: { uri: plugin.uri },
|
|
||||||
range: diag.range,
|
|
||||||
context: {
|
|
||||||
diagnostics: [diag], // TODO multiple?
|
|
||||||
triggerKind: lsp.CodeActionTriggerKind.Invoked,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return plugin.client.request<
|
|
||||||
lsp.CodeActionParams,
|
|
||||||
(lsp.Command | lsp.CodeAction)[]
|
|
||||||
>(
|
|
||||||
"textDocument/codeAction",
|
|
||||||
params
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function serverDiagnostics(): LSPClientExtension {
|
|
||||||
return {
|
|
||||||
clientCapabilities: {
|
|
||||||
textDocument: { publishDiagnostics: { versionSupport: true } },
|
|
||||||
},
|
|
||||||
notificationHandlers: {
|
|
||||||
"textDocument/publishDiagnostics": (
|
|
||||||
client,
|
|
||||||
params: lsp.PublishDiagnosticsParams,
|
|
||||||
) => {
|
|
||||||
const file = client.workspace.getFile(params.uri) as OpenFile;
|
|
||||||
if (!file) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (params.version != null && params.version != file.version) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
file.setDiagnostics(
|
|
||||||
params.diagnostics.map((item) => ({
|
|
||||||
from: file.changes.mapPos(
|
|
||||||
fromPosition(file.doc, item.range.start),
|
|
||||||
),
|
|
||||||
to: file.changes.mapPos(
|
|
||||||
fromPosition(file.doc, item.range.end),
|
|
||||||
),
|
|
||||||
severity: toSeverity(item.severity ?? 1),
|
|
||||||
message: item.message,
|
|
||||||
actions: [{
|
|
||||||
name: "Solve",
|
|
||||||
apply: async (view) => {
|
|
||||||
const plugin = LSPPlugin.get(view);
|
|
||||||
const a = await fetchCodeActionsForDiagnostic(plugin, item);
|
|
||||||
const effects = setCodeActions.of(a);
|
|
||||||
view.dispatch({ effects });
|
|
||||||
},
|
|
||||||
}]
|
|
||||||
})),
|
|
||||||
);
|
|
||||||
return true;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
editorExtension: [autoSync, codeActionState],
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
@ -2,7 +2,7 @@ import van from "vanjs-core";
|
||||||
const v = van.tags;
|
const v = van.tags;
|
||||||
|
|
||||||
export const Button = (onclick: () => void, text: string) =>
|
export const Button = (onclick: () => void, text: string) =>
|
||||||
v.button({ class: "bg-green-500 dark:bg-green-700 p-2", onclick }, text);
|
v.button({ class: "bg-green-500 p-2", onclick }, text);
|
||||||
|
|
||||||
export const InlineButton = (
|
export const InlineButton = (
|
||||||
onclick: () => void,
|
onclick: () => void,
|
||||||
|
|
|
||||||
|
|
@ -18,10 +18,7 @@ const lspServers = new Map<string, LspEntry>();
|
||||||
// simple fallback mapping for a few languages — prefer env overrides
|
// simple fallback mapping for a few languages — prefer env overrides
|
||||||
const fallbackServerForLanguage: Record<string, string | undefined> = {
|
const fallbackServerForLanguage: Record<string, string | undefined> = {
|
||||||
typescript: "npx typescript-language-server --log-level 4 --stdio",
|
typescript: "npx typescript-language-server --log-level 4 --stdio",
|
||||||
python: "pylsp --check-parent-process",
|
python: "pylsp",
|
||||||
haskell: "haskell-language-server-wrapper --lsp",
|
|
||||||
rust: "rust-analyzer",
|
|
||||||
cpp: "clangd",
|
|
||||||
};
|
};
|
||||||
|
|
||||||
function ensureLspForKey(
|
function ensureLspForKey(
|
||||||
|
|
@ -60,6 +57,8 @@ function ensureLspForKey(
|
||||||
serverKey,
|
serverKey,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
console.log("Current environment: ", process.env);
|
||||||
|
|
||||||
// Spawn using shell:true so PATH and shell resolution behave like a user shell.
|
// Spawn using shell:true so PATH and shell resolution behave like a user shell.
|
||||||
const proc = spawn([cmd].concat(args).join(" "), {
|
const proc = spawn([cmd].concat(args).join(" "), {
|
||||||
stdio: ["pipe", "pipe", "pipe"],
|
stdio: ["pipe", "pipe", "pipe"],
|
||||||
|
|
@ -87,9 +86,7 @@ function ensureLspForKey(
|
||||||
const len = parseInt(m[1], 10);
|
const len = parseInt(m[1], 10);
|
||||||
const totalLen = headerEnd + 4 + len;
|
const totalLen = headerEnd + 4 + len;
|
||||||
if (entry.buffer.length < totalLen) break; // wait for more
|
if (entry.buffer.length < totalLen) break; // wait for more
|
||||||
const body = entry.buffer
|
const body = entry.buffer.subarray(headerEnd + 4, totalLen).toString();
|
||||||
.subarray(headerEnd + 4, totalLen)
|
|
||||||
.toString();
|
|
||||||
// Forward body to all attached ports
|
// Forward body to all attached ports
|
||||||
try {
|
try {
|
||||||
entry.ports.forEach((p) => {
|
entry.ports.forEach((p) => {
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,6 @@ import { terminalManager } from "./pty";
|
||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import started from "electron-squirrel-startup";
|
import started from "electron-squirrel-startup";
|
||||||
import { setupLangServer } from "./langserver";
|
import { setupLangServer } from "./langserver";
|
||||||
import { setMenu } from "./menu";
|
|
||||||
/// <reference types="./forge-vite-env.d.ts" />
|
/// <reference types="./forge-vite-env.d.ts" />
|
||||||
|
|
||||||
// Handle creating/removing shortcuts on Windows when installing/uninstalling.
|
// Handle creating/removing shortcuts on Windows when installing/uninstalling.
|
||||||
|
|
@ -22,12 +21,11 @@ if (started) {
|
||||||
}
|
}
|
||||||
|
|
||||||
app.setName("miller");
|
app.setName("miller");
|
||||||
setMenu();
|
|
||||||
|
|
||||||
const createWindow = () => {
|
const createWindow = () => {
|
||||||
// Create the browser window.
|
// Create the browser window.
|
||||||
const mainWindow = new BrowserWindow({
|
const mainWindow = new BrowserWindow({
|
||||||
width: 1280,
|
width: 1152,
|
||||||
height: 720,
|
height: 720,
|
||||||
webPreferences: {
|
webPreferences: {
|
||||||
preload: path.join(__dirname, "preload.js"),
|
preload: path.join(__dirname, "preload.js"),
|
||||||
|
|
|
||||||
114
src/main/menu.ts
114
src/main/menu.ts
|
|
@ -1,114 +0,0 @@
|
||||||
const { shell } = require("electron/common");
|
|
||||||
const { app, Menu } = require("electron/main");
|
|
||||||
|
|
||||||
const isMac = process.platform === "darwin";
|
|
||||||
const menuTemplate = [
|
|
||||||
// { role: 'appMenu' }
|
|
||||||
...(isMac
|
|
||||||
? [
|
|
||||||
{
|
|
||||||
label: app.name,
|
|
||||||
submenu: [
|
|
||||||
{ role: "about" },
|
|
||||||
{ type: "separator" },
|
|
||||||
{ role: "services" },
|
|
||||||
{ type: "separator" },
|
|
||||||
{ role: "hide" },
|
|
||||||
{ role: "hideOthers" },
|
|
||||||
{ role: "unhide" },
|
|
||||||
{ type: "separator" },
|
|
||||||
{ role: "quit" },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
]
|
|
||||||
: []),
|
|
||||||
// { role: 'fileMenu' }
|
|
||||||
{
|
|
||||||
label: "File",
|
|
||||||
submenu: [isMac ? { role: "close" } : { role: "quit" }],
|
|
||||||
},
|
|
||||||
// { role: 'editMenu' }
|
|
||||||
// {
|
|
||||||
// label: "Edit",
|
|
||||||
// submenu: [
|
|
||||||
// { role: "undo" },
|
|
||||||
// { role: "redo" },
|
|
||||||
// { type: "separator" },
|
|
||||||
// { role: "cut" },
|
|
||||||
// { role: "copy" },
|
|
||||||
// { role: "paste" },
|
|
||||||
// ...(isMac
|
|
||||||
// ? [
|
|
||||||
// { role: "pasteAndMatchStyle" },
|
|
||||||
// { role: "delete" },
|
|
||||||
// { role: "selectAll" },
|
|
||||||
// { type: "separator" },
|
|
||||||
// {
|
|
||||||
// label: "Speech",
|
|
||||||
// submenu: [
|
|
||||||
// { role: "startSpeaking" },
|
|
||||||
// { role: "stopSpeaking" },
|
|
||||||
// ],
|
|
||||||
// },
|
|
||||||
// ]
|
|
||||||
// : [
|
|
||||||
// { role: "delete" },
|
|
||||||
// { type: "separator" },
|
|
||||||
// { role: "selectAll" },
|
|
||||||
// ]),
|
|
||||||
// ],
|
|
||||||
// },
|
|
||||||
// { role: 'viewMenu' }
|
|
||||||
{
|
|
||||||
label: "View",
|
|
||||||
submenu: [
|
|
||||||
{ role: "toggleDevTools" },
|
|
||||||
{ type: "separator" },
|
|
||||||
{ role: "resetZoom" },
|
|
||||||
{ role: "zoomIn" },
|
|
||||||
{ role: "zoomOut" },
|
|
||||||
{ type: "separator" },
|
|
||||||
{ role: "togglefullscreen" },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
// { role: 'windowMenu' }
|
|
||||||
{
|
|
||||||
label: "Window",
|
|
||||||
submenu: [
|
|
||||||
{ role: "minimize" },
|
|
||||||
{ role: "zoom" },
|
|
||||||
...(isMac
|
|
||||||
? [
|
|
||||||
{ type: "separator" },
|
|
||||||
{ role: "front" },
|
|
||||||
{ type: "separator" },
|
|
||||||
{ role: "window" },
|
|
||||||
]
|
|
||||||
: []),
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
role: "help",
|
|
||||||
submenu: [
|
|
||||||
{
|
|
||||||
label: "Learn More",
|
|
||||||
click: async () => {
|
|
||||||
const { shell } = require("electron");
|
|
||||||
await shell.openExternal("https://electronjs.org");
|
|
||||||
},
|
|
||||||
},
|
|
||||||
...(!isMac
|
|
||||||
? [
|
|
||||||
{
|
|
||||||
role: "about",
|
|
||||||
},
|
|
||||||
]
|
|
||||||
: []),
|
|
||||||
],
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
export function setMenu() {
|
|
||||||
const menu = Menu.buildFromTemplate(menuTemplate);
|
|
||||||
Menu.setApplicationMenu(menu);
|
|
||||||
}
|
|
||||||
Loading…
Reference in New Issue