Skip to content

Commit

Permalink
Merge pull request #184 from crystal-lang-tools/nobody/small-error-ha…
Browse files Browse the repository at this point in the history
…ndling

Release 0.9.1
  • Loading branch information
nobodywasishere authored Feb 7, 2024
2 parents 41ae652 + 898ac1f commit c016a9f
Show file tree
Hide file tree
Showing 13 changed files with 799 additions and 708 deletions.
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
# Change Log

## [0.9.1] - 2024-02-06

### Fix

- V 0.9 won't catch some errors [#183](https://github.com/crystal-lang-tools/vscode-crystal-lang/issues/183)
- Spawn problem tool if `crystal tool dependencies` failed
- General formatting

## [0.9.0] - 2024-02-01

### Fix
Expand Down
84 changes: 72 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,23 +1,83 @@
# VSCode Extension for Crystal Language

![Publish Extension](https://github.com/crystal-lang-tools/vscode-crystal-lang/workflows/Publish%20Extension/badge.svg)
---
**This is the official version of the plugin for Crystal Language support in VS Code.**
**Remove all old versions before installing this one.**
---

This extension provides support for the [Crystal](https://github.com/crystal-lang) programming language.

![vscode-crystal-lang](https://i.imgur.com/ZxIsOWB.gif)
![vscode-crystal-lang](./images/vscode-example.gif)

## Wiki
## Requirements

1. [Requirements](https://github.com/crystal-lang-tools/vscode-crystal-lang/wiki/Requirements)
2. [Features](https://github.com/crystal-lang-tools/vscode-crystal-lang/wiki/Features)
3. [Settings](https://github.com/crystal-lang-tools/vscode-crystal-lang/wiki/Settings)
4. [Useful extensions](https://github.com/crystal-lang-tools/vscode-crystal-lang/wiki/Useful-extensions)
5. [Known issues](https://github.com/crystal-lang-tools/vscode-crystal-lang/wiki/Known-Issues)
6. [Roadmap](https://github.com/crystal-lang-tools/vscode-crystal-lang/wiki/Roadmap)
It is recommended to install the [Crystal programming language](https://crystal-lang.org/) (platform dependendant). No other dependencies are required.
For debugging support, it's recommended to follow the guide [here](https://dev.to/bcardiff/debug-crystal-in-vscode-via-codelldb-3lf).

## Features

- Syntax highlighting:
- Syntax highlighting support for Crystal, [Slang](https://github.com/jeromegn/slang), and [ECR](https://crystal-lang.org/api/latest/ECR.html) (contributions for other Crystal templating languages welcome)
- Auto indentation:
- Automatically indent while coding (after `do`, `if`, etc.)
- Snippets:
- Helpful code completions for common use-cases
- Formatting
- Allows for "format on save" and manual formatting, and works even if the editor isn't saved to disk
- Problems finder
- When enabled, on opening or saving a file the project will be compiled to find any problems with the code, and these problems are reported in the editor. Depending on the project this can be slow and memory intensive, so there is an option to disable it.
- Document symbols
- Allows for easier code navigation through breadcrumbs at the top of the file, as well as being able to view and jump to the documents symbols
- Peek / go to definition
- When enabled, this allows for quickly jumping to the definition(s) for a method using the `crystal tool implementations` command
- Show type on hover
- When enabled, this allows for viewing the type of a variable or return type of a method on hover, using the `crystal tool context` command
- Tasks
- Enables executing various `crystal`/`shards` commands directly from VSCode

## Settings

- `compiler` - set a custom absolute path for the Crystal compiler
- `definitions` - enables jump-to-definition (reload required)
- `dependencies` - use the dependencies tool to determine the main for each file, required if there's multiple entrypoints to the project, can be slow
- `flags` - flags to pass to the compiler
- `hover` - show type information on hover (reload required)
- `main` - set a main executable to use for the current project (`${workspaceRoot}/src/main.cr`)
- `problems` - runs the compiler on save and reports any issues (reload required)
- `server` - absolute path to an LSP executable to use instead of the custom features provided by this extension, like [Crystalline](https://github.com/elbywan/crystalline) (reload required)
- `shards` - set a custom absolute path for the shards executable
- `spec-explorer` - enable the built-in testing UI for specs, recommended for Crystal >= 1.11 due to `--dry-run` flag (reload required)
- `spec-tags` - specific tags to pass to the spec runner

By default, the problems runner, hover provider, and definitions provider are turned on. This may not be ideal for larger projects due to compile times and memory usage, so it is recommended to turn them off in the vscode settings. That can be done per-project by creating a `.vscode/settings.json` file with:

```json
// .vscode/settings.json
{
// Turn off slow/memory-intensive features for larger projects
"crystal-lang.definitions": false,
"crystal-lang.dependencies": false,
"crystal-lang.hover": false,
"crystal-lang.problems": false,
"crystal-lang.spec-explorer": false,
}
```

## Supported Platforms

This extension has been tested on / should work on the following platforms:

- Linux (Arch / Ubuntu / Fedora)
- MacOS (Intel / Apple Silicon)
- Windows (10 Native / 10 WSL2 / 11)
- GitHub Codespaces

## Roadmap

These are some features that are planned or would be nice for the future of this project or others:

- Better / faster LSP support
- Better completion algorithm
- Better symbol detection
- Integrated debugger support
- Refactored task runner

## Release Notes

Expand Down
Binary file added images/vscode-example.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"name": "crystal-lang",
"displayName": "Crystal Language",
"description": "The Crystal Programming Language",
"version": "0.9.0",
"version": "0.9.1",
"publisher": "crystal-lang-tools",
"icon": "images/icon.gif",
"license": "MIT",
Expand Down
2 changes: 2 additions & 0 deletions src/definitions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,8 @@ async function spawnImplTool(
const config = workspace.getConfiguration('crystal-lang');
const cursor = getCursorPath(document, position);
const main = await getShardMainPath(document);
if (!main) return;

const cmd = `${shellEscape(compiler)} tool implementations -c ${shellEscape(cursor)} ${shellEscape(main)} -f json --no-color ${config.get<string>("flags")}`
const folder: WorkspaceFolder = getWorkspaceFolder(document.uri)

Expand Down
10 changes: 5 additions & 5 deletions src/format.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,14 +85,14 @@ async function spawnFormatTool(document: TextDocument): Promise<string> {

child.on('close', () => {
if (err.length > 0) {
const err_resp = err.join('')
const err_resp = err.join('') + "\n" + out.join('')
findProblemsRaw(err_resp, document.uri)
rej(err_resp);
return;
} else {
const out_resp = out.join('')
findProblemsRaw(out_resp, document.uri)
res(out_resp);
}
const out_resp = out.join('')
findProblemsRaw(out_resp, document.uri)
res(out_resp);
})
});
}
2 changes: 2 additions & 0 deletions src/hover.ts
Original file line number Diff line number Diff line change
Expand Up @@ -269,6 +269,8 @@ async function spawnContextTool(
// Spec files shouldn't have main set to something in src/
// but are instead their own main files
const main = await getShardMainPath(document);
if (!main) return;

const cmd = `${shellEscape(compiler)} tool context -c ${shellEscape(cursor)} ${shellEscape(main)} -f json --no-color ${config.get<string>("flags")}`
const folder = getWorkspaceFolder(document.uri)

Expand Down
80 changes: 41 additions & 39 deletions src/macro.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,49 +4,51 @@ import { crystalOutputChannel, execAsync, findProblems, getCompilerPath, getCurs
export const macroOutputChannel = window.createOutputChannel("Crystal Macro", "markdown")

export function registerMacroExpansion() {
commands.registerCommand('crystal-lang.showMacroExpansion', async function () {
const activeEditor = window.activeTextEditor;
const document = activeEditor.document;
const position = activeEditor.selection.active;

const response = await spawnMacroExpandTool(document, position)
.then((resp) => {
if (resp) {
return resp
} else {
return undefined
}
})

if (response) {
macroOutputChannel.appendLine("```crystal\n" + response + "```\n")
commands.registerCommand('crystal-lang.showMacroExpansion', async function () {
const activeEditor = window.activeTextEditor;
const document = activeEditor.document;
const position = activeEditor.selection.active;

const response = await spawnMacroExpandTool(document, position)
.then((resp) => {
if (resp) {
return resp
} else {
macroOutputChannel.appendLine("# No macro expansion found")
return undefined
}
macroOutputChannel.show()
})
})

if (response) {
macroOutputChannel.appendLine("```crystal\n" + response + "```\n")
} else {
macroOutputChannel.appendLine("# No macro expansion found")
}
macroOutputChannel.show()
})
}

export async function spawnMacroExpandTool(document: TextDocument, position: Position): Promise<string | void> {
const compiler = await getCompilerPath();
const main = await getShardMainPath(document);
const cursor = getCursorPath(document, position);
const folder = getWorkspaceFolder(document.uri);
const config = workspace.getConfiguration('crystal-lang');

const cmd = `${shellEscape(compiler)} tool expand ${shellEscape(main)} --cursor ${shellEscape(cursor)} ${config.get<string>("flags")}`

crystalOutputChannel.appendLine(`[Macro Expansion] (${folder.name}) $ ` + cmd)
return await execAsync(cmd, folder.uri.fsPath)
.then((response) => {
return response;
const compiler = await getCompilerPath();
const main = await getShardMainPath(document);
if (!main) return;

const cursor = getCursorPath(document, position);
const folder = getWorkspaceFolder(document.uri);
const config = workspace.getConfiguration('crystal-lang');

const cmd = `${shellEscape(compiler)} tool expand ${shellEscape(main)} --cursor ${shellEscape(cursor)} ${config.get<string>("flags")}`

crystalOutputChannel.appendLine(`[Macro Expansion] (${folder.name}) $ ` + cmd)
return await execAsync(cmd, folder.uri.fsPath)
.then((response) => {
return response;
})
.catch(async (err) => {
const new_cmd = cmd + ' -f json'
await execAsync(new_cmd, folder.uri.fsPath)
.catch((err) => {
findProblems(err.stderr, document.uri)
crystalOutputChannel.appendLine(`[Macro Expansion] Error: ${err.message}`)
})
.catch(async (err) => {
const new_cmd = cmd + ' -f json'
await execAsync(new_cmd, folder.uri.fsPath)
.catch((err) => {
findProblems(err.stderr, document.uri)
crystalOutputChannel.appendLine(`[Macro Expansion] Error: ${err.message}`)
})
});
});
}
118 changes: 65 additions & 53 deletions src/problems.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,59 +2,71 @@ import { TextDocument, workspace } from "vscode";
import { setStatusBar, compiler_mutex, crystalOutputChannel, diagnosticCollection, execAsync, findProblems, getCompilerPath, getShardMainPath, getWorkspaceFolder, shellEscape } from "./tools";

export function registerProblems(): void {
workspace.onDidSaveTextDocument((e) => {
if (e.uri && e.uri.scheme === "file" && (e.fileName.endsWith(".cr") || e.fileName.endsWith(".ecr"))) {
if (compiler_mutex.isLocked()) return;

const dispose = setStatusBar('finding problems...');

compiler_mutex.acquire()
.then((release) => {
spawnProblemsTool(e)
.catch((err) => {
crystalOutputChannel.appendLine(`[Problems] Error: ${JSON.stringify(err)}`)
})
.finally(() => {
release()
})
})
.finally(() => {
dispose()
})
}
})

return;
workspace.onDidOpenTextDocument((e) => handleDocument(e))
workspace.onDidSaveTextDocument((e) => handleDocument(e))

return;
}

/**
* Determines whether a document should have the problems tool run on it. If it should,
* acquires the compiler mutex and executes the problems tool.
*
* @param {TextDocument} document
* @return {*} {Promise<void>}
*/
async function handleDocument(document: TextDocument): Promise<void> {
if (document.uri && document.uri.scheme === "file" && (document.fileName.endsWith(".cr") || document.fileName.endsWith(".ecr"))) {
if (compiler_mutex.isLocked()) return;

const dispose = setStatusBar('finding problems...');

compiler_mutex.acquire()
.then((release) => {
spawnProblemsTool(document)
.catch((err) => {
crystalOutputChannel.appendLine(`[Problems] Error: ${JSON.stringify(err)}`)
})
.finally(() => {
release()
})
})
.finally(() => {
dispose()
})
}
}

async function spawnProblemsTool(document: TextDocument): Promise<void> {
const compiler = await getCompilerPath();
const main = await getShardMainPath(document);
const folder = getWorkspaceFolder(document.uri).uri.fsPath;
const config = workspace.getConfiguration('crystal-lang');

// If document is in a folder of the same name as the document, it will throw an
// error about not being able to use an output filename of '...' as it's a folder.
// This is probably a bug as the --no-codegen flag is set, there is no output.
//
// Error: can't use `...` as output filename because it's a directory
//
const output = process.platform === "win32" ? "nul" : "/dev/null"

const cmd = `${shellEscape(compiler)} build ${shellEscape(main)} --no-debug --no-color --no-codegen --error-trace -f json -o ${output} ${config.get<string>("flags")}`

crystalOutputChannel.appendLine(`[Problems] (${getWorkspaceFolder(document.uri).name}) $ ` + cmd)
await execAsync(cmd, folder)
.then((response) => {
diagnosticCollection.clear()
crystalOutputChannel.appendLine("[Problems] No problems found.")
}).catch((err) => {
findProblems(err.stderr, document.uri)
try {
const parsed = JSON.parse(err.stderr)
crystalOutputChannel.appendLine(`[Problems] Error: ${err.stderr}`)
} catch {
crystalOutputChannel.appendLine(`[Problems] Error: ${JSON.stringify(err)}`)
}
});
export async function spawnProblemsTool(document: TextDocument, mainFile: string = undefined): Promise<void> {
const compiler = await getCompilerPath();
const main = mainFile || await getShardMainPath(document);
if (!main) return;

const folder = getWorkspaceFolder(document.uri).uri.fsPath;
const config = workspace.getConfiguration('crystal-lang');

// If document is in a folder of the same name as the document, it will throw an
// error about not being able to use an output filename of '...' as it's a folder.
// This is probably a bug as the --no-codegen flag is set, there is no output.
//
// Error: can't use `...` as output filename because it's a directory
//
const output = process.platform === "win32" ? "nul" : "/dev/null"

const cmd = `${shellEscape(compiler)} build ${shellEscape(main)} --no-debug --no-color --no-codegen --error-trace -f json -o ${output} ${config.get<string>("flags")}`

crystalOutputChannel.appendLine(`[Problems] (${getWorkspaceFolder(document.uri).name}) $ ` + cmd)
await execAsync(cmd, folder)
.then((response) => {
diagnosticCollection.clear()
crystalOutputChannel.appendLine("[Problems] No problems found.")
}).catch((err) => {
findProblems(err.stderr, document.uri)
try {
const parsed = JSON.parse(err.stderr)
crystalOutputChannel.appendLine(`[Problems] Error: ${err.stderr}`)
} catch {
crystalOutputChannel.appendLine(`[Problems] Error: ${JSON.stringify(err)}`)
}
});
}
Loading

0 comments on commit c016a9f

Please sign in to comment.