mirror of
https://github.com/Strum355/mcshader-lsp.git
synced 2025-08-06 18:08:56 +00:00
Compare commits
60 commits
0.9.2
...
rust-rewri
Author | SHA1 | Date | |
---|---|---|---|
![]() |
85cbb6d81e | ||
![]() |
fb12c9b144 | ||
![]() |
4dd5542355 | ||
![]() |
78b6a6ef1d | ||
![]() |
b6da5c97fb | ||
![]() |
d1d1e2377b | ||
![]() |
05e52fc8d0 | ||
![]() |
83c86aeff2 | ||
![]() |
0768abb122 | ||
![]() |
2c2dbfb3e3 | ||
![]() |
f8cc2eed22 | ||
![]() |
c737409fde | ||
![]() |
d8d77ac600 | ||
![]() |
941822c5c7 | ||
![]() |
3b568ea087 | ||
![]() |
27d1d7b34e | ||
![]() |
3c58af95fa | ||
![]() |
f45e1a4b87 | ||
![]() |
d43bfec582 | ||
![]() |
a7cbaa198b | ||
![]() |
fecc41168a | ||
![]() |
1529460a5c | ||
![]() |
f66f56603a | ||
![]() |
d8cb0465ef | ||
![]() |
3b865dfda2 | ||
![]() |
d3365c3bff | ||
![]() |
cb7c9b8b49 | ||
![]() |
b4a0636d43 | ||
![]() |
9a499d581b | ||
![]() |
3957eaed17 | ||
![]() |
616b7cef74 | ||
![]() |
d5b0dcffb2 | ||
![]() |
f8dd31ca81 | ||
![]() |
d3c0869288 | ||
![]() |
86100aa008 | ||
![]() |
e001b4a8b1 | ||
![]() |
9a9ed21f13 | ||
![]() |
ebab8c899a | ||
![]() |
7cf009ee61 | ||
![]() |
b775bd2cd5 | ||
![]() |
a8f00fe927 | ||
![]() |
79e107b748 | ||
![]() |
cccb5e25f7 | ||
![]() |
554777d0da | ||
![]() |
5747a9d9b1 | ||
![]() |
57f4b7924b | ||
![]() |
5fd02b06f4 | ||
![]() |
65422c863f | ||
![]() |
734f0b014b | ||
![]() |
c7d8b02ee3 | ||
![]() |
db5e5afb26 | ||
![]() |
b649aeb1f6 | ||
![]() |
248afcd988 | ||
![]() |
c854093a96 | ||
![]() |
551380a6ed | ||
![]() |
9a770f69a4 | ||
![]() |
22deb53ecd | ||
![]() |
3bfa7a2cc4 | ||
![]() |
72ea905413 | ||
![]() |
fabbc68fd7 |
83 changed files with 10981 additions and 5284 deletions
1
.gitattributes
vendored
1
.gitattributes
vendored
|
@ -1 +1,2 @@
|
||||||
* text eol=lf
|
* text eol=lf
|
||||||
|
*.png binary
|
21
.github/workflows/extension.yml
vendored
Normal file
21
.github/workflows/extension.yml
vendored
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
name: Build Extension
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [ rust-rewrite ]
|
||||||
|
pull_request:
|
||||||
|
branches: [ rust-rewrite ]
|
||||||
|
jobs:
|
||||||
|
build-vscode-extension:
|
||||||
|
runs-on: ubuntu-20.04
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v2
|
||||||
|
- run: npm i
|
||||||
|
- uses: HaaLeo/publish-vscode-extension@v0
|
||||||
|
id: vsce_build
|
||||||
|
with:
|
||||||
|
pat: 'sample text'
|
||||||
|
dryRun: true
|
||||||
|
- uses: actions/upload-artifact@v2
|
||||||
|
with:
|
||||||
|
name: vscode-mc-shader.vsix
|
||||||
|
path: ${{ steps.vsce_build.outputs.vsixPath }}
|
14
.github/workflows/lsif.yml
vendored
Normal file
14
.github/workflows/lsif.yml
vendored
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
name: LSIF
|
||||||
|
on:
|
||||||
|
- push
|
||||||
|
jobs:
|
||||||
|
index:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v2
|
||||||
|
- name: Generate LSIF data
|
||||||
|
uses: sourcegraph/lsif-rust-action@main
|
||||||
|
- name: Upload LSIF data
|
||||||
|
uses: sourcegraph/lsif-upload-action@master
|
||||||
|
with:
|
||||||
|
github_token: ${{ secrets.GITHUB_TOKEN }}
|
17
.github/workflows/release.yml
vendored
17
.github/workflows/release.yml
vendored
|
@ -35,12 +35,19 @@ jobs:
|
||||||
- os: ubuntu-18.04
|
- os: ubuntu-18.04
|
||||||
target: x86_64-unknown-linux-gnu
|
target: x86_64-unknown-linux-gnu
|
||||||
dir: server/mcshader-lsp
|
dir: server/mcshader-lsp
|
||||||
|
artifact: x86_64-unknown-linux-gnu
|
||||||
- os: windows-latest
|
- os: windows-latest
|
||||||
target: x86_64-windows-msvc.exe
|
target: x86_64-pc-windows-msvc
|
||||||
dir: server/mcshader-lsp.exe
|
dir: server/mcshader-lsp.exe
|
||||||
- os: macos-10.15
|
artifact: x86_64-windows-msvc.exe
|
||||||
|
- os: macos-11
|
||||||
target: x86_64-apple-darwin
|
target: x86_64-apple-darwin
|
||||||
dir: server/mcshader-lsp
|
dir: server/mcshader-lsp
|
||||||
|
artifact: x86_64-apple-darwin
|
||||||
|
- os: macos-11
|
||||||
|
target: aarch64-apple-darwin
|
||||||
|
dir: server/mcshader-lsp
|
||||||
|
artifact: aarch64-apple-darwin
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v2
|
||||||
- name: Install latest nightly
|
- name: Install latest nightly
|
||||||
|
@ -48,8 +55,10 @@ jobs:
|
||||||
with:
|
with:
|
||||||
toolchain: nightly
|
toolchain: nightly
|
||||||
default: true
|
default: true
|
||||||
|
target: ${{ matrix.platforms.target }}
|
||||||
|
override: true
|
||||||
- name: Build server
|
- name: Build server
|
||||||
run: cargo build --release --out-dir . -Z unstable-options
|
run: cargo build --release --target ${{ matrix.platforms.target }} --out-dir . -Z unstable-options
|
||||||
- name: Upload release file
|
- name: Upload release file
|
||||||
uses: actions/upload-release-asset@v1
|
uses: actions/upload-release-asset@v1
|
||||||
env:
|
env:
|
||||||
|
@ -57,7 +66,7 @@ jobs:
|
||||||
with:
|
with:
|
||||||
upload_url: ${{ needs.empty-release.outputs.upload_url }}
|
upload_url: ${{ needs.empty-release.outputs.upload_url }}
|
||||||
asset_path: ${{ matrix.platforms.dir }}
|
asset_path: ${{ matrix.platforms.dir }}
|
||||||
asset_name: mcshader-lsp-${{ matrix.platforms.target }}
|
asset_name: mcshader-lsp-${{ matrix.platforms.artifact }}
|
||||||
asset_content_type: application/octet-stream
|
asset_content_type: application/octet-stream
|
||||||
release-vscode-extension:
|
release-vscode-extension:
|
||||||
runs-on: ubuntu-20.04
|
runs-on: ubuntu-20.04
|
||||||
|
|
36
.github/workflows/server.yml
vendored
36
.github/workflows/server.yml
vendored
|
@ -8,16 +8,44 @@ env:
|
||||||
CARGO_TERM_COLOR: always
|
CARGO_TERM_COLOR: always
|
||||||
jobs:
|
jobs:
|
||||||
build-and-test:
|
build-and-test:
|
||||||
runs-on: ${{ matrix.os }}
|
runs-on: ${{ matrix.platforms.os }}
|
||||||
defaults:
|
defaults:
|
||||||
run:
|
run:
|
||||||
working-directory: server
|
working-directory: server
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
os: [ ubuntu-20.04, windows-latest, macos-10.15 ]
|
platforms:
|
||||||
|
- os: ubuntu-18.04
|
||||||
|
target: x86_64-unknown-linux-gnu
|
||||||
|
dir: server/mcshader-lsp
|
||||||
|
artifact: x86_64-unknown-linux-gnu
|
||||||
|
- os: windows-latest
|
||||||
|
target: x86_64-pc-windows-msvc
|
||||||
|
dir: server/mcshader-lsp.exe
|
||||||
|
artifact: x86_64-windows-msvc.exe
|
||||||
|
- os: macos-11
|
||||||
|
target: x86_64-apple-darwin
|
||||||
|
dir: server/mcshader-lsp
|
||||||
|
artifact: x86_64-apple-darwin
|
||||||
|
- os: macos-11
|
||||||
|
target: aarch64-apple-darwin
|
||||||
|
dir: server/mcshader-lsp
|
||||||
|
artifact: aarch64-apple-darwin
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v2
|
||||||
|
- name: Install latest nightly
|
||||||
|
uses: actions-rs/toolchain@v1
|
||||||
|
with:
|
||||||
|
toolchain: nightly
|
||||||
|
default: true
|
||||||
|
target: ${{ matrix.platforms.target }}
|
||||||
|
override: true
|
||||||
- name: Build server
|
- name: Build server
|
||||||
run: cargo build
|
run: cargo build --target ${{ matrix.platforms.target }} --out-dir . -Z unstable-options
|
||||||
|
- uses: actions/upload-artifact@v2
|
||||||
|
with:
|
||||||
|
name: mcshader-lsp-${{ matrix.platforms.artifact }}
|
||||||
|
path: ${{ matrix.platforms.dir }}
|
||||||
- name: Run tests
|
- name: Run tests
|
||||||
run: cargo test
|
run: cargo test --target ${{ matrix.platforms.target }}
|
||||||
|
if: ${{ matrix.platforms.target != 'aarch64-apple-darwin' }}
|
||||||
|
|
3
.rustfmt.toml
Normal file
3
.rustfmt.toml
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
edition = "2021"
|
||||||
|
fn_args_layout = "compressed"
|
||||||
|
max_width = 140
|
100
CHANGELOG.md
100
CHANGELOG.md
|
@ -4,6 +4,106 @@ All notable changes to the "vscode-mc-shader" extension will be documented in th
|
||||||
|
|
||||||
The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)
|
The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)
|
||||||
|
|
||||||
|
## [0.9.9]
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Support for mod world folders, outside the standard world{-1,0,1}.
|
||||||
|
- Support for compute shader files ending in \_a to \_z.
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Crash when running with eglot as LSP client.
|
||||||
|
- Extension icon client not displaying (encoding issue).
|
||||||
|
|
||||||
|
## [0.9.8]
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- NVIDIA diagnostics line offset off-by-one due to confusion with erroneous (non-proper) GLSL files resulting in both -1 and -2 offsets appearing to be valid when only the former is.
|
||||||
|
- Non-toplevel files being treated as toplevel files when they have .fsh/.vsh/etc and not imported into a valid toplevel file.
|
||||||
|
- Fix issue in the depth-first-search iterator when a file is imported twice into another file with a different include in between.
|
||||||
|
|
||||||
|
## [0.9.7]
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Fixed bad release tag format
|
||||||
|
- Fixed extension silently failing on activation
|
||||||
|
|
||||||
|
## [0.9.6]
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- MacOS M1 binary releases
|
||||||
|
- AMD OpenGL driver diagnostics output support. AMD linting is a-go 🚀
|
||||||
|
- Tree-sitter based go-to-definition/find-references/document symbols. Currently disabled until stabilized
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Another `#include` merging bug when a file is imported twice into another file at different lines
|
||||||
|
|
||||||
|
## [0.9.5]
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Filesystem watcher reads custom defined file associations
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Fixed `#include` merging for when file is merged twice that would normally be `#ifdef` guarded. Please see commit message of [551380a](https://github.com/Strum355/mcshader-lsp/commit/551380a6ed00709287460b7d8c88e7803956052c) for detailed explanation
|
||||||
|
|
||||||
|
## [0.9.4]
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- `#include` merging when project consists of files with both CRLF and LF files
|
||||||
|
- Out-of-tree shader files are not linted or added to the dependency graph
|
||||||
|
- Client no longer attempts to bootstrap server when `MCSHADER_DEBUG=true`
|
||||||
|
|
||||||
|
## [0.9.3]
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Language server download for windows
|
||||||
|
|
||||||
|
## [0.9.2]
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- VSCode extension activation predicate to only when `shaders` folder exists at top level
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Additional client-side logging
|
||||||
|
|
||||||
|
## [0.9.1]
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Windows support in client not adding `.exe` to language server path
|
||||||
|
- Binary release CI
|
||||||
|
|
||||||
|
## [0.9.0]
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Replaced in-process Typescript language server with Rust based language server
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Due to the above, `#include` directive handling is vastly improved
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Command to view read-only document representing a top-level file with all includes merged
|
||||||
|
- Command to generate a DOT graph file of the entire project
|
||||||
|
- Command to restart language server
|
||||||
|
|
||||||
|
### Removed
|
||||||
|
|
||||||
|
- `glslangValidatorPath` and `shaderpacksPath` config settings
|
||||||
|
|
||||||
## [0.8.5]
|
## [0.8.5]
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
# Minecraft GLSL Shaders Language Server
|
# Minecraft GLSL Shaders Language Server
|
||||||
## mcshader-lsp
|
## mcshader-lsp
|
||||||
|
|
||||||
[](https://marketplace.visualstudio.com/items?itemName=strum355.vscode-mc-shader) [](https://marketplace.visualstudio.com/items?itemName=strum355.vscode-mc-shader)
|
[](https://marketplace.visualstudio.com/items?itemName=strum355.vscode-mc-shader) [](https://marketplace.visualstudio.com/items?itemName=strum355.vscode-mc-shader)
|
||||||
[](https://github.com/Strum355/mcshader-lsp)
|
[](https://github.com/Strum355/mcshader-lsp)
|
||||||
[](https://github.com/Strum355/mcshader-lsp/issues)
|
[](https://github.com/Strum355/mcshader-lsp/issues)
|
||||||
[](https://cloud.drone.io/Strum355/mcshader-lsp)
|
[](https://cloud.drone.io/Strum355/mcshader-lsp)
|
||||||
|
@ -12,7 +12,7 @@ Currently supported editors:
|
||||||
|
|
||||||
- [Visual Studio Code](https://code.visualstudio.com/) with `vscode-mc-shader`
|
- [Visual Studio Code](https://code.visualstudio.com/) with `vscode-mc-shader`
|
||||||
|
|
||||||
<img src="https://github.com/Strum355/mcshader-lsp/raw/master/logo.png" width="20%" height="20%">
|
<img src="https://github.com/Strum355/mcshader-lsp/raw/rust-rewrite/logo.png" width="20%" height="20%">
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
|
|
1312
client/package-lock.json
generated
1312
client/package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
@ -5,16 +5,18 @@
|
||||||
"rollup": "rollup -c"
|
"rollup": "rollup -c"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"adm-zip": "^0.4.14",
|
"@rollup/plugin-json": "^4.1.0",
|
||||||
"node-fetch": "^2.6.0",
|
"adm-zip": "^0.5.9",
|
||||||
"vscode-languageclient": "^6.1.3"
|
"encoding": "^0.1.13",
|
||||||
|
"node-fetch": "^2.6.7",
|
||||||
|
"vscode-languageclient": "^6.1.4"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"rollup": "^2.38.1",
|
"@rollup/plugin-commonjs": "^21.0.2",
|
||||||
"@rollup/plugin-commonjs": "^17.1.0",
|
"@rollup/plugin-node-resolve": "^13.1.3",
|
||||||
"@rollup/plugin-node-resolve": "^11.1.1",
|
"@types/adm-zip": "^0.4.34",
|
||||||
"@types/vscode": "^1.47.0",
|
"@types/node-fetch": "^2.6.1",
|
||||||
"@types/adm-zip": "^0.4.32",
|
"@types/vscode": "^1.65.0",
|
||||||
"@types/node-fetch": "^2.5.4"
|
"rollup": "^2.70.1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,11 +1,13 @@
|
||||||
import resolve from '@rollup/plugin-node-resolve';
|
import resolve from '@rollup/plugin-node-resolve';
|
||||||
import commonjs from '@rollup/plugin-commonjs';
|
import commonjs from '@rollup/plugin-commonjs';
|
||||||
|
import json from '@rollup/plugin-json';
|
||||||
import nodeBuiltins from 'builtin-modules';
|
import nodeBuiltins from 'builtin-modules';
|
||||||
|
|
||||||
/** @type { import('rollup').RollupOptions } */
|
/** @type { import('rollup').RollupOptions } */
|
||||||
export default {
|
export default {
|
||||||
input: 'out/extension.js',
|
input: 'out/extension.js',
|
||||||
plugins: [
|
plugins: [
|
||||||
|
json(),
|
||||||
resolve({
|
resolve({
|
||||||
preferBuiltins: true
|
preferBuiltins: true
|
||||||
}),
|
}),
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
import path = require('path')
|
||||||
import * as vscode from 'vscode'
|
import * as vscode from 'vscode'
|
||||||
import * as lsp from 'vscode-languageclient'
|
import * as lsp from 'vscode-languageclient'
|
||||||
import { Extension } from './extension'
|
import { Extension } from './extension'
|
||||||
|
@ -30,7 +31,7 @@ export function virtualMergedDocument(e: Extension): Command {
|
||||||
command: 'virtualMerge',
|
command: 'virtualMerge',
|
||||||
arguments: [path]
|
arguments: [path]
|
||||||
})
|
})
|
||||||
} catch(e) {}
|
} catch (e) { }
|
||||||
|
|
||||||
return content
|
return content
|
||||||
}
|
}
|
||||||
|
@ -40,17 +41,67 @@ export function virtualMergedDocument(e: Extension): Command {
|
||||||
onDidChange = this.onDidChangeEmitter.event
|
onDidChange = this.onDidChangeEmitter.event
|
||||||
|
|
||||||
provideTextDocumentContent(uri: vscode.Uri, __: vscode.CancellationToken): vscode.ProviderResult<string> {
|
provideTextDocumentContent(uri: vscode.Uri, __: vscode.CancellationToken): vscode.ProviderResult<string> {
|
||||||
return getVirtualDocument(uri.path)
|
return getVirtualDocument(uri.path.replace('.flattened' + path.extname(uri.path), path.extname(uri.path)))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
e.context.subscriptions.push(vscode.workspace.registerTextDocumentContentProvider('mcglsl', docProvider))
|
e.context.subscriptions.push(vscode.workspace.registerTextDocumentContentProvider('mcglsl', docProvider))
|
||||||
|
|
||||||
return async () => {
|
return async () => {
|
||||||
const uri = vscode.window.activeTextEditor.document.uri
|
if (vscode.window.activeTextEditor.document.languageId != 'glsl') return
|
||||||
const path = vscode.Uri.parse('mcglsl:' + uri.path)
|
|
||||||
|
const uri = vscode.window.activeTextEditor.document.uri.path
|
||||||
|
.substring(0, vscode.window.activeTextEditor.document.uri.path.lastIndexOf('.'))
|
||||||
|
+ '.flattened.'
|
||||||
|
+ vscode.window.activeTextEditor.document.uri.path
|
||||||
|
.slice(vscode.window.activeTextEditor.document.uri.path.lastIndexOf('.') + 1)
|
||||||
|
const path = vscode.Uri.parse(`mcglsl:${uri}`)
|
||||||
|
|
||||||
const doc = await vscode.workspace.openTextDocument(path)
|
const doc = await vscode.workspace.openTextDocument(path)
|
||||||
docProvider.onDidChangeEmitter.fire(path)
|
docProvider.onDidChangeEmitter.fire(path)
|
||||||
await vscode.window.showTextDocument(doc, {preview: true})
|
await vscode.window.showTextDocument(doc, {
|
||||||
|
viewColumn: vscode.ViewColumn.Two,
|
||||||
|
preview: true
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function parseTree(e: Extension): Command {
|
||||||
|
const getVirtualDocument = async (path: string): Promise<string | null> => {
|
||||||
|
let content: string = ''
|
||||||
|
try {
|
||||||
|
content = await e.lspClient.sendRequest<string>(lsp.ExecuteCommandRequest.type.method, {
|
||||||
|
command: 'parseTree',
|
||||||
|
arguments: [path]
|
||||||
|
})
|
||||||
|
} catch (e) { }
|
||||||
|
|
||||||
|
return content
|
||||||
|
}
|
||||||
|
|
||||||
|
const docProvider = new class implements vscode.TextDocumentContentProvider {
|
||||||
|
onDidChangeEmitter = new vscode.EventEmitter<vscode.Uri>()
|
||||||
|
onDidChange = this.onDidChangeEmitter.event
|
||||||
|
|
||||||
|
provideTextDocumentContent(uri: vscode.Uri, _: vscode.CancellationToken): vscode.ProviderResult<string> {
|
||||||
|
if (uri.path.includes('.flattened.')) return ''
|
||||||
|
return getVirtualDocument(uri.path.substring(0, uri.path.lastIndexOf('.')))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
e.context.subscriptions.push(vscode.workspace.registerTextDocumentContentProvider('mcglsl', docProvider))
|
||||||
|
|
||||||
|
return async () => {
|
||||||
|
if (vscode.window.activeTextEditor.document.languageId != 'glsl') return
|
||||||
|
|
||||||
|
const uri = vscode.window.activeTextEditor.document.uri
|
||||||
|
const path = vscode.Uri.parse(`mcglsl:${uri.path}.ast`)
|
||||||
|
|
||||||
|
const doc = await vscode.workspace.openTextDocument(path)
|
||||||
|
docProvider.onDidChangeEmitter.fire(path)
|
||||||
|
await vscode.window.showTextDocument(doc, {
|
||||||
|
viewColumn: vscode.ViewColumn.Two,
|
||||||
|
preview: true
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -9,9 +9,10 @@ import { PersistentState } from './persistent_state'
|
||||||
import * as path from 'path'
|
import * as path from 'path'
|
||||||
|
|
||||||
const platforms: { [key: string]: string } = {
|
const platforms: { [key: string]: string } = {
|
||||||
'x64 win32': 'x86_64-pc-windows-msvc',
|
'x64 win32': 'x86_64-windows-msvc',
|
||||||
'x64 linux': 'x86_64-unknown-linux-gnu',
|
'x64 linux': 'x86_64-unknown-linux-gnu',
|
||||||
'x64 darwin': 'x86_64-apple-darwin',
|
'x64 darwin': 'x86_64-apple-darwin',
|
||||||
|
'arm64 darwin': 'aarch64-apple-darwin'
|
||||||
}
|
}
|
||||||
|
|
||||||
export class Extension {
|
export class Extension {
|
||||||
|
@ -24,7 +25,7 @@ export class Extension {
|
||||||
|
|
||||||
readonly package: {
|
readonly package: {
|
||||||
version: string
|
version: string
|
||||||
} = vscode.extensions.getExtension(this.extensionID)!.packageJSON;
|
} = vscode.extensions.getExtension(this.extensionID)!.packageJSON
|
||||||
|
|
||||||
public get context(): vscode.ExtensionContext {
|
public get context(): vscode.ExtensionContext {
|
||||||
return this.extensionContext
|
return this.extensionContext
|
||||||
|
@ -38,27 +39,58 @@ export class Extension {
|
||||||
this.extensionContext = context
|
this.extensionContext = context
|
||||||
this.state = new PersistentState(context.globalState)
|
this.state = new PersistentState(context.globalState)
|
||||||
|
|
||||||
await this.bootstrap()
|
if (!process.env['MCSHADER_DEBUG'] && !(vscode.workspace.getConfiguration('mcglsl').get('skipBootstrap') as boolean)) {
|
||||||
|
await this.bootstrap()
|
||||||
|
} else {
|
||||||
|
log.info('skipping language server bootstrap')
|
||||||
|
}
|
||||||
|
|
||||||
this.registerCommand('graphDot', commands.generateGraphDot)
|
this.registerCommand('graphDot', commands.generateGraphDot)
|
||||||
this.registerCommand('restart', commands.restartExtension)
|
this.registerCommand('restart', commands.restartExtension)
|
||||||
this.registerCommand('virtualMerge', commands.virtualMergedDocument)
|
this.registerCommand('virtualMerge', commands.virtualMergedDocument)
|
||||||
|
this.registerCommand('parseTree', commands.parseTree)
|
||||||
|
|
||||||
log.info('starting language server...')
|
log.info('starting language server...')
|
||||||
|
|
||||||
this.client = await new LanguageClient(this).startServer()
|
const lspBinary = process.env['MCSHADER_DEBUG'] ?
|
||||||
|
this.context.asAbsolutePath(path.join('server', 'target', 'debug', 'mcshader-lsp')) +
|
||||||
|
(process.platform === 'win32' ? '.exe' : '') :
|
||||||
|
path.join(this.context.globalStorageUri.fsPath, 'mcshader-lsp')
|
||||||
|
|
||||||
|
const filewatcherGlob = this.fileAssociationsToGlob(this.getGLSLFileAssociations())
|
||||||
|
|
||||||
|
this.client = await new LanguageClient(this, lspBinary, filewatcherGlob).startServer()
|
||||||
|
|
||||||
log.info('language server started!')
|
log.info('language server started!')
|
||||||
}
|
}
|
||||||
|
|
||||||
registerCommand = (name: string, f: (e: Extension) => commands.Command) => {
|
fileAssociationsToGlob = (associations: string[]): string => {
|
||||||
const cmd = f(this)
|
return '**/*.{'.concat(
|
||||||
this.context.subscriptions.push(vscode.commands.registerCommand('mcglsl.'+name, cmd))
|
associations.map(s => s.substring(s.indexOf('.'))).join(',')
|
||||||
|
) + '}'
|
||||||
}
|
}
|
||||||
|
|
||||||
deactivate = async () => {
|
getGLSLFileAssociations = (): string[] => {
|
||||||
|
const exts = ['.fsh', '.vsh', '.gsh', '.glsl']
|
||||||
|
const associations = vscode.workspace.getConfiguration('files').get('associations') as { [key: string]: string }
|
||||||
|
|
||||||
|
Object.keys(associations).forEach((key) => {
|
||||||
|
if (associations[key] === 'glsl') {
|
||||||
|
exts.push(key.substring(key.indexOf('*') + 1))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return exts
|
||||||
|
}
|
||||||
|
|
||||||
|
registerCommand = (name: string, f: (e: Extension) => commands.Command) => {
|
||||||
|
const cmd = f(this)
|
||||||
|
this.context.subscriptions.push(vscode.commands.registerCommand('mcglsl.' + name, cmd))
|
||||||
|
}
|
||||||
|
|
||||||
|
deactivate = async () => {
|
||||||
await this.lspClient.stop()
|
await this.lspClient.stop()
|
||||||
while(this.context.subscriptions.length > 0) {
|
while (this.context.subscriptions.length > 0) {
|
||||||
this.context.subscriptions.pop()?.dispose()
|
this.context.subscriptions.pop()?.dispose()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -83,7 +115,7 @@ export class Extension {
|
||||||
if (!exists) await this.state.updateServerVersion(undefined)
|
if (!exists) await this.state.updateServerVersion(undefined)
|
||||||
|
|
||||||
const release = await getReleaseInfo(this.package.version)
|
const release = await getReleaseInfo(this.package.version)
|
||||||
log.info(`got release info from Github:\n\t`, JSON.stringify(release))
|
log.info('got release info from Github:\n\t', JSON.stringify(release))
|
||||||
|
|
||||||
const platform = platforms[`${process.arch} ${process.platform}`]
|
const platform = platforms[`${process.arch} ${process.platform}`]
|
||||||
if (platform === undefined) {
|
if (platform === undefined) {
|
||||||
|
@ -93,7 +125,7 @@ export class Extension {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (release.tag_name === this.state.serverVersion) {
|
if (release.tag_name === this.state.serverVersion) {
|
||||||
log.info(`server version is same as extension:\n\t`, this.state.serverVersion)
|
log.info('server version is same as extension:\n\t', this.state.serverVersion)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -103,8 +135,8 @@ export class Extension {
|
||||||
|
|
||||||
const userResponse = await vscode.window.showInformationMessage(
|
const userResponse = await vscode.window.showInformationMessage(
|
||||||
this.state.serverVersion == undefined ?
|
this.state.serverVersion == undefined ?
|
||||||
`Language server version ${this.package.version} is not installed.` :
|
`Language server version ${this.package.version} is not installed.` :
|
||||||
`An update is available. Upgrade from ${this.state.serverVersion} to ${release.tag_name}?`,
|
`An update is available. Upgrade from ${this.state.serverVersion} to ${release.tag_name}?`,
|
||||||
'Download now'
|
'Download now'
|
||||||
)
|
)
|
||||||
if (userResponse !== 'Download now') {
|
if (userResponse !== 'Download now') {
|
||||||
|
@ -118,4 +150,11 @@ export class Extension {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const activate = new Extension().activate
|
export const activate = async (context: vscode.ExtensionContext) => {
|
||||||
|
try {
|
||||||
|
new Extension().activate(context)
|
||||||
|
} catch (e) {
|
||||||
|
log.error(`failed to activate extension: ${e}`)
|
||||||
|
throw(e)
|
||||||
|
}
|
||||||
|
}
|
|
@ -9,33 +9,33 @@ export const log = new class {
|
||||||
|
|
||||||
// Hint: the type [T, ...T[]] means a non-empty array
|
// Hint: the type [T, ...T[]] means a non-empty array
|
||||||
debug(...msg: [unknown, ...unknown[]]): void {
|
debug(...msg: [unknown, ...unknown[]]): void {
|
||||||
log.write('DEBUG', ...msg)
|
log.write('DEBUG', ...msg)
|
||||||
}
|
}
|
||||||
|
|
||||||
info(...msg: [unknown, ...unknown[]]): void {
|
info(...msg: [unknown, ...unknown[]]): void {
|
||||||
log.write('INFO ', ...msg)
|
log.write('INFO ', ...msg)
|
||||||
}
|
}
|
||||||
|
|
||||||
warn(...msg: [unknown, ...unknown[]]): void {
|
warn(...msg: [unknown, ...unknown[]]): void {
|
||||||
log.write('WARN ', ...msg)
|
log.write('WARN ', ...msg)
|
||||||
}
|
}
|
||||||
|
|
||||||
error(...msg: [unknown, ...unknown[]]): void {
|
error(...msg: [unknown, ...unknown[]]): void {
|
||||||
log.write('ERROR', ...msg)
|
log.write('ERROR', ...msg)
|
||||||
}
|
}
|
||||||
|
|
||||||
write(label: string, ...messageParts: unknown[]): void {
|
write(label: string, ...messageParts: unknown[]): void {
|
||||||
const message = messageParts.map(log.stringify).join(' ')
|
const message = messageParts.map(log.stringify).join(' ')
|
||||||
const dateTime = new Date().toLocaleString()
|
const dateTime = new Date().toLocaleString()
|
||||||
log.output.appendLine(`${label} [${dateTime}]: ${message}`)
|
log.output.appendLine(`${label} [${dateTime}]: ${message}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
private stringify(val: unknown): string {
|
private stringify(val: unknown): string {
|
||||||
if (typeof val === 'string') return val
|
if (typeof val === 'string') return val
|
||||||
return inspect(val, {
|
return inspect(val, {
|
||||||
colors: false,
|
colors: false,
|
||||||
depth: 6, // heuristic
|
depth: 6, // heuristic
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,28 +1,27 @@
|
||||||
import * as path from 'path'
|
|
||||||
import { ConfigurationTarget, workspace } from 'vscode'
|
import { ConfigurationTarget, workspace } from 'vscode'
|
||||||
import * as lsp from 'vscode-languageclient'
|
import * as lsp from 'vscode-languageclient'
|
||||||
import { Extension } from './extension'
|
import { Extension } from './extension'
|
||||||
import { lspOutputChannel } from './log'
|
import { log, lspOutputChannel } from './log'
|
||||||
import { ConfigUpdateParams, statusMethod, StatusParams, updateConfigMethod } from './lspExt'
|
import { ConfigUpdateParams, statusMethod, StatusParams, updateConfigMethod } from './lspExt'
|
||||||
|
|
||||||
export class LanguageClient extends lsp.LanguageClient {
|
export class LanguageClient extends lsp.LanguageClient {
|
||||||
private extension: Extension
|
private extension: Extension
|
||||||
|
|
||||||
constructor(ext: Extension) {
|
constructor(ext: Extension, lspBinary: string, filewatcherGlob: string) {
|
||||||
super('vscode-mc-shader', 'VSCode MC Shader', {
|
super('vscode-mc-shader', 'VSCode MC Shader', {
|
||||||
command: process.env['MCSHADER_DEBUG'] ?
|
command: lspBinary
|
||||||
ext.context.asAbsolutePath(path.join('server', 'target', 'debug', 'mcshader-lsp')) +
|
|
||||||
(process.platform === 'win32' ? '.exe' : '') :
|
|
||||||
path.join(ext.context.globalStoragePath, 'mcshader-lsp')
|
|
||||||
}, {
|
}, {
|
||||||
documentSelector: [{scheme: 'file', language: 'glsl'}],
|
documentSelector: [{ scheme: 'file', language: 'glsl' }],
|
||||||
outputChannel: lspOutputChannel,
|
outputChannel: lspOutputChannel,
|
||||||
synchronize: {
|
synchronize: {
|
||||||
configurationSection: 'mcglsl',
|
configurationSection: 'mcglsl',
|
||||||
fileEvents: workspace.createFileSystemWatcher('**/*.{fsh,gsh,vsh,glsl,inc}')
|
fileEvents: workspace.createFileSystemWatcher(filewatcherGlob)
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
this.extension = ext
|
this.extension = ext
|
||||||
|
|
||||||
|
log.info('server receiving events for file glob:\n\t', filewatcherGlob)
|
||||||
|
log.info('running with binary at path:\n\t', lspBinary)
|
||||||
}
|
}
|
||||||
|
|
||||||
public startServer = async (): Promise<LanguageClient> => {
|
public startServer = async (): Promise<LanguageClient> => {
|
||||||
|
|
|
@ -12,5 +12,5 @@ export const status = new lsp.NotificationType<StatusParams>(statusMethod)
|
||||||
export const updateConfigMethod = 'mc-glsl/updateConfig'
|
export const updateConfigMethod = 'mc-glsl/updateConfig'
|
||||||
|
|
||||||
export type ConfigUpdateParams = {
|
export type ConfigUpdateParams = {
|
||||||
kv: {key: string, value: string}[]
|
kv: { key: string, value: string }[]
|
||||||
}
|
}
|
|
@ -16,8 +16,9 @@ interface GithubRelease {
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getReleaseInfo(releaseTag: string): Promise<GithubRelease> {
|
export async function getReleaseInfo(releaseTag: string): Promise<GithubRelease> {
|
||||||
|
log.info('fetching release info for tag', releaseTag)
|
||||||
const response = await fetch(`https://api.github.com/repos/strum355/mcshader-lsp/releases/tags/${releaseTag}`, {
|
const response = await fetch(`https://api.github.com/repos/strum355/mcshader-lsp/releases/tags/${releaseTag}`, {
|
||||||
headers: {Accept: 'application/vnd.github.v3+json'}
|
headers: { Accept: 'application/vnd.github.v3+json' }
|
||||||
})
|
})
|
||||||
|
|
||||||
const isRelease = (obj: unknown): obj is GithubRelease => {
|
const isRelease = (obj: unknown): obj is GithubRelease => {
|
||||||
|
@ -28,8 +29,8 @@ export async function getReleaseInfo(releaseTag: string): Promise<GithubRelease>
|
||||||
}
|
}
|
||||||
|
|
||||||
const json = await response.json()
|
const json = await response.json()
|
||||||
if(!isRelease(json)) {
|
if (!isRelease(json)) {
|
||||||
throw new TypeError('Received malformed request from Github Release API ' + JSON.stringify(json))
|
throw new TypeError(`Received malformed request from Github Release API ${JSON.stringify(json)}`)
|
||||||
}
|
}
|
||||||
return json
|
return json
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,6 +12,6 @@ export class PersistentState {
|
||||||
}
|
}
|
||||||
|
|
||||||
async updateServerVersion(value: string | undefined) {
|
async updateServerVersion(value: string | undefined) {
|
||||||
await this.state.update('serverVersion', value)
|
await this.state.update('serverVersion', value)
|
||||||
}
|
}
|
||||||
}
|
}
|
BIN
logo-min.png
Normal file
BIN
logo-min.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 24 KiB |
BIN
logo-mini.png
Normal file
BIN
logo-mini.png
Normal file
Binary file not shown.
BIN
logo.png
BIN
logo.png
Binary file not shown.
Before Width: | Height: | Size: 1.1 MiB After Width: | Height: | Size: 1.1 MiB |
6766
package-lock.json
generated
6766
package-lock.json
generated
File diff suppressed because it is too large
Load diff
53
package.json
53
package.json
|
@ -2,16 +2,16 @@
|
||||||
"name": "vscode-mc-shader",
|
"name": "vscode-mc-shader",
|
||||||
"displayName": "Minecraft GLSL Shaders",
|
"displayName": "Minecraft GLSL Shaders",
|
||||||
"description": "A Visual Studio Code extension for linting/etc Minecraft GLSL Shaders",
|
"description": "A Visual Studio Code extension for linting/etc Minecraft GLSL Shaders",
|
||||||
"version": "0.9.2",
|
"version": "0.9.9",
|
||||||
"publisher": "Strum355",
|
"publisher": "Strum355",
|
||||||
"author": "Noah Santschi-Cooney (Strum355)",
|
"author": "Noah Santschi-Cooney (Strum355)",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"icon": "logo.png",
|
"icon": "logo-min.png",
|
||||||
"repository": {
|
"repository": {
|
||||||
"url": "https://github.com/Strum355/vscode-mc-shader"
|
"url": "https://github.com/Strum355/mcshader-lsp"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"vscode": "^1.43.0"
|
"vscode": "^1.53.0"
|
||||||
},
|
},
|
||||||
"categories": [
|
"categories": [
|
||||||
"Linters",
|
"Linters",
|
||||||
|
@ -41,6 +41,11 @@
|
||||||
"command": "mcglsl.virtualMerge",
|
"command": "mcglsl.virtualMerge",
|
||||||
"title": "Show flattened file",
|
"title": "Show flattened file",
|
||||||
"category": "Minecraft Shader"
|
"category": "Minecraft Shader"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"command": "mcglsl.parseTree",
|
||||||
|
"title": "Show parse tree for file",
|
||||||
|
"category": "Minecraft Shader"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"languages": [
|
"languages": [
|
||||||
|
@ -59,7 +64,19 @@
|
||||||
],
|
],
|
||||||
"configuration": {
|
"configuration": {
|
||||||
"title": "Minecraft GLSL Shaders",
|
"title": "Minecraft GLSL Shaders",
|
||||||
"properties": {}
|
"properties": {
|
||||||
|
"mcglsl.skipBootstrap": {
|
||||||
|
"type": "boolean",
|
||||||
|
"default": false,
|
||||||
|
"description": "[DEBUG] Enable to skip bootstrapping the language server binary from Github. Set this to use a manually provided language server binary."
|
||||||
|
},
|
||||||
|
"mcglsl.logLevel": {
|
||||||
|
"type": "string",
|
||||||
|
"default": "info",
|
||||||
|
"enum": ["trace", "debug", "info", "warn", "error"],
|
||||||
|
"description": "Change the log level of the language server. This change happens live and does not require a restart."
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
@ -72,22 +89,28 @@
|
||||||
"fix": "eslint 'client/**/*.ts' --fix"
|
"fix": "eslint 'client/**/*.ts' --fix"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/node": "^10.14.15",
|
"@types/node": "^17.0.21",
|
||||||
"@typescript-eslint/parser": "^3.6.1",
|
"@typescript-eslint/parser": "^5.15.0",
|
||||||
"concurrently": "^5.1.0",
|
"concurrently": "^7.0.0",
|
||||||
"eslint": "^7.4.0",
|
"eslint": "^8.11.0",
|
||||||
"typescript": "^3.9.7",
|
"typescript": "^4.6.2",
|
||||||
"vsce": "^1.77.0"
|
"vsce": "^2.7.0"
|
||||||
},
|
},
|
||||||
"eslintConfig": {
|
"eslintConfig": {
|
||||||
"parser": "@typescript-eslint/parser",
|
"parser": "@typescript-eslint/parser",
|
||||||
"parserOptions": {
|
"parserOptions": {
|
||||||
"ecmaVersion": 2020,
|
"ecmaVersion": 2020,
|
||||||
"sourceType": "module"
|
"sourceType": "module"
|
||||||
},
|
},
|
||||||
"rules": {
|
"rules": {
|
||||||
"semi": ["warn", "never"],
|
"semi": [
|
||||||
"quotes": ["warn", "single"]
|
"warn",
|
||||||
|
"never"
|
||||||
|
],
|
||||||
|
"quotes": [
|
||||||
|
"warn",
|
||||||
|
"single"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
1057
server/Cargo.lock
generated
1057
server/Cargo.lock
generated
File diff suppressed because it is too large
Load diff
|
@ -1,31 +1,6 @@
|
||||||
[package]
|
[workspace]
|
||||||
name = "mcshader-lsp"
|
members = [
|
||||||
version = "0.9.2"
|
"main",
|
||||||
authors = ["Noah Santschi-Cooney <noah@santschi-cooney.ch>"]
|
"logging",
|
||||||
edition = "2018"
|
"logging_macro"
|
||||||
|
]
|
||||||
[dependencies]
|
|
||||||
rust_lsp = { git = "https://github.com/Strum355/RustLSP", branch = "master" }
|
|
||||||
serde_json = "1.0.61"
|
|
||||||
serde = "1.0.123"
|
|
||||||
walkdir = "2.3.1"
|
|
||||||
petgraph = "0.5.1"
|
|
||||||
lazy_static = "1.4.0"
|
|
||||||
regex = "1.4.3"
|
|
||||||
chan = "0.1.23"
|
|
||||||
url = "2.2.0"
|
|
||||||
percent-encoding = "2.1.0"
|
|
||||||
anyhow = "1.0.38"
|
|
||||||
bit-set = "0.5.2"
|
|
||||||
thiserror = "1.0.23"
|
|
||||||
glutin = "0.26.0"
|
|
||||||
gl = "0.14.0"
|
|
||||||
ctor = "0.1.18"
|
|
||||||
mockall = "0.9.0"
|
|
||||||
path-slash = "0.1.4"
|
|
||||||
|
|
||||||
[dev-dependencies]
|
|
||||||
tempdir = "0.3.7"
|
|
||||||
fs_extra = "1.2.0"
|
|
||||||
hamcrest2 = "*"
|
|
||||||
pretty_assertions = "0.6.1"
|
|
|
@ -4,7 +4,7 @@ watchtest:
|
||||||
RUST_BACKTRACE=0 cargo watch -x test -i Makefile
|
RUST_BACKTRACE=0 cargo watch -x test -i Makefile
|
||||||
|
|
||||||
test:
|
test:
|
||||||
RUST_LIB_BACKTRACE=0 RUST_BACKTRACE=0 cargo test
|
RUST_LIB_BACKTRACE=0 RUST_BACKTRACE=0 cargo test -- --nocapture --color always
|
||||||
|
|
||||||
build:
|
build:
|
||||||
cargo build
|
cargo build
|
||||||
|
|
13
server/logging/Cargo.toml
Normal file
13
server/logging/Cargo.toml
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
[package]
|
||||||
|
name = "logging"
|
||||||
|
version = "0.9.9"
|
||||||
|
authors = ["Noah Santschi-Cooney <noah@santschi-cooney.ch>"]
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
slog = { version = "2.7", features = [ "max_level_trace", "release_max_level_trace" ] }
|
||||||
|
slog-term = "2.9"
|
||||||
|
slog-scope = "4.4"
|
||||||
|
slog-atomic = "3.1"
|
||||||
|
rand = "0.8"
|
||||||
|
lazy_static = "1.4"
|
43
server/logging/src/lib.rs
Normal file
43
server/logging/src/lib.rs
Normal file
|
@ -0,0 +1,43 @@
|
||||||
|
use rand::{rngs, Rng};
|
||||||
|
use slog::slog_o;
|
||||||
|
use slog_scope::GlobalLoggerGuard;
|
||||||
|
use slog_term::{FullFormat, PlainSyncDecorator};
|
||||||
|
use std::{cell::RefCell, sync::Arc};
|
||||||
|
|
||||||
|
use std::io::Stderr;
|
||||||
|
|
||||||
|
use lazy_static::lazy_static;
|
||||||
|
use slog::*;
|
||||||
|
use slog_atomic::*;
|
||||||
|
|
||||||
|
fn new_trace_id() -> String {
|
||||||
|
let rng = CURRENT_RNG.with(|rng| rng.borrow_mut().gen::<[u8; 4]>());
|
||||||
|
return format!("{:04x}", u32::from_be_bytes(rng));
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn slog_with_trace_id<F: FnOnce()>(f: F) {
|
||||||
|
slog_scope::scope(&slog_scope::logger().new(slog_o!("trace" => new_trace_id())), f)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_logger_with_level(level: Level) -> GlobalLoggerGuard {
|
||||||
|
let drain = Arc::new(logger_base(level).fuse());
|
||||||
|
DRAIN_SWITCH.ctrl().set(drain.clone());
|
||||||
|
slog_scope::set_global_logger(Logger::root(drain, o!()))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn logger_base(level: Level) -> LevelFilter<Fuse<FullFormat<PlainSyncDecorator<Stderr>>>> {
|
||||||
|
let plain = slog_term::PlainSyncDecorator::new(std::io::stderr());
|
||||||
|
let drain = slog_term::FullFormat::new(plain).build().fuse();
|
||||||
|
drain.filter_level(level)
|
||||||
|
}
|
||||||
|
|
||||||
|
thread_local! {
|
||||||
|
static CURRENT_RNG: RefCell<rngs::ThreadRng> = RefCell::new(rngs::ThreadRng::default());
|
||||||
|
}
|
||||||
|
|
||||||
|
lazy_static! {
|
||||||
|
static ref DRAIN_SWITCH: AtomicSwitch<()> = {
|
||||||
|
let logger = logger_base(Level::Info).fuse();
|
||||||
|
AtomicSwitch::new(logger)
|
||||||
|
};
|
||||||
|
}
|
12
server/logging_macro/Cargo.toml
Normal file
12
server/logging_macro/Cargo.toml
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
[package]
|
||||||
|
name = "logging_macro"
|
||||||
|
version = "0.9.9"
|
||||||
|
authors = ["Noah Santschi-Cooney <noah@santschi-cooney.ch>"]
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
[lib]
|
||||||
|
proc-macro = true
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
quote = "1.0"
|
||||||
|
syn = { version = "1.0", features = [ "full" ] }
|
24
server/logging_macro/src/lib.rs
Normal file
24
server/logging_macro/src/lib.rs
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
use proc_macro::TokenStream;
|
||||||
|
use quote::quote;
|
||||||
|
use syn::{parse_macro_input, parse_quote, ItemFn};
|
||||||
|
|
||||||
|
#[proc_macro_attribute]
|
||||||
|
pub fn log_scope(_args: TokenStream, function: TokenStream) -> TokenStream {
|
||||||
|
let mut function = parse_macro_input!(function as ItemFn);
|
||||||
|
|
||||||
|
let function_name = function.sig.ident.to_string();
|
||||||
|
|
||||||
|
let stmts = function.block.stmts;
|
||||||
|
|
||||||
|
function.block = Box::new(parse_quote!({
|
||||||
|
use slog::{slog_o, FnValue, Level};
|
||||||
|
use std::thread::current;
|
||||||
|
|
||||||
|
let _guard = logging::set_logger_with_level(Level::Trace);
|
||||||
|
slog_scope::scope(&slog_scope::logger().new(slog_o!("test_name" => #function_name, "thread_num" => FnValue(|_| format!("{:?}", current().id())))), || {
|
||||||
|
#(#stmts)*
|
||||||
|
});
|
||||||
|
}));
|
||||||
|
|
||||||
|
TokenStream::from(quote!(#function))
|
||||||
|
}
|
35
server/main/Cargo.toml
Normal file
35
server/main/Cargo.toml
Normal file
|
@ -0,0 +1,35 @@
|
||||||
|
[package]
|
||||||
|
name = "mcshader-lsp"
|
||||||
|
version = "0.9.9"
|
||||||
|
authors = ["Noah Santschi-Cooney <noah@santschi-cooney.ch>"]
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
rust_lsp = { git = "https://github.com/Strum355/RustLSP", branch = "master" }
|
||||||
|
serde_json = "1.0"
|
||||||
|
serde = "1.0"
|
||||||
|
walkdir = "2.3"
|
||||||
|
petgraph = "0.6"
|
||||||
|
lazy_static = "1.4"
|
||||||
|
regex = "1.4"
|
||||||
|
url = "2.2"
|
||||||
|
percent-encoding = "2.1"
|
||||||
|
anyhow = "1.0"
|
||||||
|
thiserror = "1.0"
|
||||||
|
glutin = "0.28"
|
||||||
|
gl = "0.14"
|
||||||
|
mockall = "0.11"
|
||||||
|
path-slash = "0.1"
|
||||||
|
slog = { version = "2.7", features = [ "max_level_trace", "release_max_level_trace" ] }
|
||||||
|
slog-scope = "4.4"
|
||||||
|
once_cell = "1.7"
|
||||||
|
tree-sitter = "0.20.6"
|
||||||
|
tree-sitter-glsl = "0.1.2"
|
||||||
|
logging = { path = "../logging" }
|
||||||
|
logging_macro = { path = "../logging_macro" }
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
tempdir = "0.3"
|
||||||
|
fs_extra = "1.2"
|
||||||
|
hamcrest2 = "*"
|
||||||
|
pretty_assertions = "1.1"
|
52
server/main/src/commands/graph_dot.rs
Normal file
52
server/main/src/commands/graph_dot.rs
Normal file
|
@ -0,0 +1,52 @@
|
||||||
|
use std::cell::RefCell;
|
||||||
|
use std::fs::OpenOptions;
|
||||||
|
use std::io::prelude::*;
|
||||||
|
use std::path::Path;
|
||||||
|
use std::rc::Rc;
|
||||||
|
|
||||||
|
use petgraph::dot::Config;
|
||||||
|
use serde_json::Value;
|
||||||
|
|
||||||
|
use petgraph::dot;
|
||||||
|
|
||||||
|
use anyhow::{format_err, Result};
|
||||||
|
use slog_scope::info;
|
||||||
|
|
||||||
|
use crate::graph::CachedStableGraph;
|
||||||
|
|
||||||
|
use super::Invokeable;
|
||||||
|
|
||||||
|
pub struct GraphDotCommand {
|
||||||
|
pub graph: Rc<RefCell<CachedStableGraph>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Invokeable for GraphDotCommand {
|
||||||
|
fn run_command(&self, root: &Path, _: &[Value]) -> Result<Value> {
|
||||||
|
let filepath = root.join("graph.dot");
|
||||||
|
|
||||||
|
info!("generating dot file"; "path" => filepath.as_os_str().to_str());
|
||||||
|
|
||||||
|
let mut file = OpenOptions::new().truncate(true).write(true).create(true).open(filepath).unwrap();
|
||||||
|
|
||||||
|
let mut write_data_closure = || -> Result<(), std::io::Error> {
|
||||||
|
let graph = self.graph.as_ref();
|
||||||
|
|
||||||
|
file.seek(std::io::SeekFrom::Start(0))?;
|
||||||
|
file.write_all("digraph {\n\tgraph [splines=ortho]\n\tnode [shape=box]\n".as_bytes())?;
|
||||||
|
file.write_all(
|
||||||
|
dot::Dot::with_config(&graph.borrow().graph, &[Config::GraphContentOnly])
|
||||||
|
.to_string()
|
||||||
|
.as_bytes(),
|
||||||
|
)?;
|
||||||
|
file.write_all("\n}".as_bytes())?;
|
||||||
|
file.flush()?;
|
||||||
|
file.seek(std::io::SeekFrom::Start(0))?;
|
||||||
|
Ok(())
|
||||||
|
};
|
||||||
|
|
||||||
|
match write_data_closure() {
|
||||||
|
Err(err) => Err(format_err!("error generating graphviz data: {}", err)),
|
||||||
|
_ => Ok(Value::Null),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
114
server/main/src/commands/merged_includes.rs
Normal file
114
server/main/src/commands/merged_includes.rs
Normal file
|
@ -0,0 +1,114 @@
|
||||||
|
use std::cell::RefCell;
|
||||||
|
use std::rc::Rc;
|
||||||
|
use std::{
|
||||||
|
collections::HashMap,
|
||||||
|
path::{Path, PathBuf},
|
||||||
|
};
|
||||||
|
|
||||||
|
use serde_json::Value;
|
||||||
|
|
||||||
|
use petgraph::graph::NodeIndex;
|
||||||
|
|
||||||
|
use anyhow::{format_err, Result};
|
||||||
|
|
||||||
|
use std::fs;
|
||||||
|
|
||||||
|
use crate::dfs;
|
||||||
|
use crate::merge_views::FilialTuple;
|
||||||
|
use crate::source_mapper::SourceMapper;
|
||||||
|
use crate::{graph::CachedStableGraph, merge_views, url_norm::FromJson};
|
||||||
|
|
||||||
|
use super::Invokeable;
|
||||||
|
|
||||||
|
pub struct VirtualMergedDocument {
|
||||||
|
pub graph: Rc<RefCell<CachedStableGraph>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl VirtualMergedDocument {
|
||||||
|
// TODO: DUPLICATE CODE
|
||||||
|
fn get_file_toplevel_ancestors(&self, uri: &Path) -> Result<Option<Vec<petgraph::stable_graph::NodeIndex>>> {
|
||||||
|
let curr_node = match self.graph.borrow_mut().find_node(uri) {
|
||||||
|
Some(n) => n,
|
||||||
|
None => return Err(format_err!("node not found {:?}", uri)),
|
||||||
|
};
|
||||||
|
let roots = self.graph.borrow().collect_root_ancestors(curr_node);
|
||||||
|
if roots.is_empty() {
|
||||||
|
return Ok(None);
|
||||||
|
}
|
||||||
|
Ok(Some(roots))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_dfs_for_node(&self, root: NodeIndex) -> Result<Vec<FilialTuple>, dfs::error::CycleError> {
|
||||||
|
let graph_ref = self.graph.borrow();
|
||||||
|
|
||||||
|
let dfs = dfs::Dfs::new(&graph_ref, root);
|
||||||
|
|
||||||
|
dfs.collect::<Result<Vec<_>, _>>()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn load_sources(&self, nodes: &[FilialTuple]) -> Result<HashMap<PathBuf, String>> {
|
||||||
|
let mut sources = HashMap::new();
|
||||||
|
|
||||||
|
for node in nodes {
|
||||||
|
let graph = self.graph.borrow();
|
||||||
|
let path = graph.get_node(node.child);
|
||||||
|
|
||||||
|
if sources.contains_key(&path) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let source = match fs::read_to_string(&path) {
|
||||||
|
Ok(s) => s,
|
||||||
|
Err(e) => return Err(format_err!("error reading {:?}: {}", path, e)),
|
||||||
|
};
|
||||||
|
let source = source.replace("\r\n", "\n");
|
||||||
|
sources.insert(path.clone(), source);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(sources)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Invokeable for VirtualMergedDocument {
|
||||||
|
fn run_command(&self, root: &Path, arguments: &[Value]) -> Result<Value> {
|
||||||
|
let path = PathBuf::from_json(arguments.get(0).unwrap())?;
|
||||||
|
|
||||||
|
let file_ancestors = match self.get_file_toplevel_ancestors(&path) {
|
||||||
|
Ok(opt) => match opt {
|
||||||
|
Some(ancestors) => ancestors,
|
||||||
|
None => vec![],
|
||||||
|
},
|
||||||
|
Err(e) => return Err(e),
|
||||||
|
};
|
||||||
|
|
||||||
|
//info!("ancestors for {}:\n\t{:?}", path, file_ancestors.iter().map(|e| self.graph.borrow().graph.node_weight(*e).unwrap().clone()).collect::<Vec<String>>());
|
||||||
|
|
||||||
|
// the set of all filepath->content. TODO: change to Url?
|
||||||
|
let mut all_sources: HashMap<PathBuf, String> = HashMap::new();
|
||||||
|
|
||||||
|
// if we are a top-level file (this has to be one of the set defined by Optifine, right?)
|
||||||
|
if file_ancestors.is_empty() {
|
||||||
|
// gather the list of all descendants
|
||||||
|
let root = self.graph.borrow_mut().find_node(&path).unwrap();
|
||||||
|
let tree = match self.get_dfs_for_node(root) {
|
||||||
|
Ok(tree) => tree,
|
||||||
|
Err(e) => return Err(e.into()),
|
||||||
|
};
|
||||||
|
|
||||||
|
let sources = match self.load_sources(&tree) {
|
||||||
|
Ok(s) => s,
|
||||||
|
Err(e) => return Err(e),
|
||||||
|
};
|
||||||
|
all_sources.extend(sources);
|
||||||
|
|
||||||
|
let mut source_mapper = SourceMapper::new(all_sources.len());
|
||||||
|
let graph = self.graph.borrow();
|
||||||
|
let view = merge_views::MergeViewBuilder::new(&tree, &all_sources, &graph, &mut source_mapper).build();
|
||||||
|
return Ok(serde_json::value::Value::String(view));
|
||||||
|
}
|
||||||
|
return Err(format_err!(
|
||||||
|
"{:?} is not a top-level file aka has ancestors",
|
||||||
|
path.strip_prefix(root).unwrap()
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
36
server/main/src/commands/mod.rs
Normal file
36
server/main/src/commands/mod.rs
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
use std::{collections::HashMap, path::Path};
|
||||||
|
|
||||||
|
use serde_json::Value;
|
||||||
|
|
||||||
|
use anyhow::{format_err, Result};
|
||||||
|
use slog_scope::info;
|
||||||
|
|
||||||
|
pub mod graph_dot;
|
||||||
|
pub mod merged_includes;
|
||||||
|
pub mod parse_tree;
|
||||||
|
|
||||||
|
pub struct CustomCommandProvider {
|
||||||
|
commands: HashMap<String, Box<dyn Invokeable>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CustomCommandProvider {
|
||||||
|
pub fn new(commands: Vec<(&str, Box<dyn Invokeable>)>) -> CustomCommandProvider {
|
||||||
|
CustomCommandProvider {
|
||||||
|
commands: commands.into_iter().map(|tup| (tup.0.into(), tup.1)).collect(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn execute(&self, command: &str, args: &[Value], root_path: &Path) -> Result<Value> {
|
||||||
|
if self.commands.contains_key(command) {
|
||||||
|
info!("running command";
|
||||||
|
"command" => command,
|
||||||
|
"args" => format!("[{}]", args.iter().map(|v| serde_json::to_string(v).unwrap()).collect::<Vec<String>>().join(", ")));
|
||||||
|
return self.commands.get(command).unwrap().run_command(root_path, args);
|
||||||
|
}
|
||||||
|
Err(format_err!("command doesn't exist"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait Invokeable {
|
||||||
|
fn run_command(&self, root: &Path, arguments: &[Value]) -> Result<Value>;
|
||||||
|
}
|
94
server/main/src/commands/parse_tree.rs
Normal file
94
server/main/src/commands/parse_tree.rs
Normal file
|
@ -0,0 +1,94 @@
|
||||||
|
use std::{
|
||||||
|
cell::RefCell,
|
||||||
|
fs,
|
||||||
|
path::{Path, PathBuf},
|
||||||
|
rc::Rc,
|
||||||
|
};
|
||||||
|
|
||||||
|
use anyhow::{format_err, Result};
|
||||||
|
use serde_json::Value;
|
||||||
|
use slog_scope::warn;
|
||||||
|
use tree_sitter::{Parser, TreeCursor};
|
||||||
|
|
||||||
|
use crate::url_norm::FromJson;
|
||||||
|
|
||||||
|
use super::Invokeable;
|
||||||
|
|
||||||
|
pub struct TreeSitterSExpr {
|
||||||
|
pub tree_sitter: Rc<RefCell<Parser>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Invokeable for TreeSitterSExpr {
|
||||||
|
fn run_command(&self, _: &Path, arguments: &[Value]) -> Result<Value> {
|
||||||
|
let path = PathBuf::from_json(arguments.get(0).unwrap())?;
|
||||||
|
|
||||||
|
warn!("parsing"; "path" => path.to_str().unwrap().to_string());
|
||||||
|
|
||||||
|
let source = fs::read_to_string(path)?;
|
||||||
|
|
||||||
|
let tree = match self.tree_sitter.borrow_mut().parse(source, None) {
|
||||||
|
Some(tree) => tree,
|
||||||
|
None => return Err(format_err!("tree-sitter parsing resulted in no parse tree")),
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut cursor = tree.walk();
|
||||||
|
|
||||||
|
let rendered = render_parse_tree(&mut cursor);
|
||||||
|
|
||||||
|
Ok(serde_json::value::Value::String(rendered))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_parse_tree(cursor: &mut TreeCursor) -> String {
|
||||||
|
let mut string = String::new();
|
||||||
|
|
||||||
|
let mut indent = 0;
|
||||||
|
let mut visited_children = false;
|
||||||
|
|
||||||
|
loop {
|
||||||
|
let node = cursor.node();
|
||||||
|
|
||||||
|
let display_name = if node.is_missing() {
|
||||||
|
format!("MISSING {}", node.kind())
|
||||||
|
} else if node.is_named() {
|
||||||
|
node.kind().to_string()
|
||||||
|
} else {
|
||||||
|
"".to_string()
|
||||||
|
};
|
||||||
|
|
||||||
|
if visited_children {
|
||||||
|
if cursor.goto_next_sibling() {
|
||||||
|
visited_children = false;
|
||||||
|
} else if cursor.goto_parent() {
|
||||||
|
visited_children = true;
|
||||||
|
indent -= 1;
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if !display_name.is_empty() {
|
||||||
|
let start = node.start_position();
|
||||||
|
let end = node.end_position();
|
||||||
|
|
||||||
|
let field_name = match cursor.field_name() {
|
||||||
|
Some(name) => name.to_string() + ": ",
|
||||||
|
None => "".to_string(),
|
||||||
|
};
|
||||||
|
|
||||||
|
string += (" ".repeat(indent)
|
||||||
|
+ format!("{}{} [{}, {}] - [{}, {}]\n", field_name, display_name, start.row, start.column, end.row, end.column)
|
||||||
|
.trim_start())
|
||||||
|
.as_str();
|
||||||
|
}
|
||||||
|
|
||||||
|
if cursor.goto_first_child() {
|
||||||
|
visited_children = false;
|
||||||
|
indent += 1;
|
||||||
|
} else {
|
||||||
|
visited_children = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
string
|
||||||
|
}
|
12
server/main/src/configuration.rs
Normal file
12
server/main/src/configuration.rs
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
use std::str::FromStr;
|
||||||
|
|
||||||
|
use slog::Level;
|
||||||
|
use slog_scope::error;
|
||||||
|
|
||||||
|
|
||||||
|
pub fn handle_log_level_change<F: FnOnce(Level)>(log_level: String, callback: F) {
|
||||||
|
match Level::from_str(log_level.as_str()) {
|
||||||
|
Ok(level) => callback(level),
|
||||||
|
Err(_) => error!("got unexpected log level from config"; "level" => log_level),
|
||||||
|
};
|
||||||
|
}
|
335
server/main/src/dfs.rs
Normal file
335
server/main/src/dfs.rs
Normal file
|
@ -0,0 +1,335 @@
|
||||||
|
use petgraph::stable_graph::NodeIndex;
|
||||||
|
|
||||||
|
use crate::{graph::CachedStableGraph, merge_views::FilialTuple};
|
||||||
|
|
||||||
|
use anyhow::Result;
|
||||||
|
|
||||||
|
struct VisitCount {
|
||||||
|
node: NodeIndex,
|
||||||
|
touch: usize,
|
||||||
|
children: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Performs a depth-first search with duplicates
|
||||||
|
pub struct Dfs<'a> {
|
||||||
|
stack: Vec<NodeIndex>,
|
||||||
|
graph: &'a CachedStableGraph,
|
||||||
|
cycle: Vec<VisitCount>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> Dfs<'a> {
|
||||||
|
pub fn new(graph: &'a CachedStableGraph, start: NodeIndex) -> Self {
|
||||||
|
Dfs {
|
||||||
|
stack: vec![start],
|
||||||
|
graph,
|
||||||
|
cycle: Vec::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn reset_path_to_branch(&mut self) {
|
||||||
|
while let Some(par) = self.cycle.last_mut() {
|
||||||
|
par.touch += 1;
|
||||||
|
if par.touch > par.children {
|
||||||
|
self.cycle.pop();
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn check_for_cycle(&self, children: &[NodeIndex]) -> Result<(), error::CycleError> {
|
||||||
|
for prev in &self.cycle {
|
||||||
|
for child in children {
|
||||||
|
if prev.node == *child {
|
||||||
|
let cycle_nodes: Vec<NodeIndex> = self.cycle.iter().map(|n| n.node).collect();
|
||||||
|
return Err(error::CycleError::new(&cycle_nodes, *child, self.graph));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> Iterator for Dfs<'a> {
|
||||||
|
type Item = Result<FilialTuple, error::CycleError>;
|
||||||
|
|
||||||
|
fn next(&mut self) -> Option<Result<FilialTuple, error::CycleError>> {
|
||||||
|
let parent = self.cycle.last().map(|p| p.node);
|
||||||
|
|
||||||
|
if let Some(child) = self.stack.pop() {
|
||||||
|
self.cycle.push(VisitCount {
|
||||||
|
node: child,
|
||||||
|
children: self.graph.graph.edges(child).count(),
|
||||||
|
touch: 1,
|
||||||
|
});
|
||||||
|
|
||||||
|
let mut children: Vec<_> = self
|
||||||
|
.graph
|
||||||
|
.get_all_child_positions(child)
|
||||||
|
.collect();
|
||||||
|
children.reverse();
|
||||||
|
|
||||||
|
if !children.is_empty() {
|
||||||
|
|
||||||
|
let child_indexes: Vec<_> = children.iter().map(|c| c.0).collect();
|
||||||
|
match self.check_for_cycle(&child_indexes) {
|
||||||
|
Ok(_) => {}
|
||||||
|
Err(e) => return Some(Err(e)),
|
||||||
|
};
|
||||||
|
|
||||||
|
for child in children {
|
||||||
|
self.stack.push(child.0);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
self.reset_path_to_branch();
|
||||||
|
}
|
||||||
|
|
||||||
|
return Some(Ok(FilialTuple { child, parent }));
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub mod error {
|
||||||
|
use petgraph::stable_graph::NodeIndex;
|
||||||
|
|
||||||
|
use std::{
|
||||||
|
error::Error as StdError,
|
||||||
|
fmt::{Debug, Display},
|
||||||
|
path::PathBuf,
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::{consts, graph::CachedStableGraph};
|
||||||
|
|
||||||
|
use rust_lsp::lsp_types::{Diagnostic, DiagnosticSeverity, Position, Range};
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct CycleError(Vec<PathBuf>);
|
||||||
|
|
||||||
|
impl StdError for CycleError {}
|
||||||
|
|
||||||
|
impl CycleError {
|
||||||
|
pub fn new(nodes: &[NodeIndex], current_node: NodeIndex, graph: &CachedStableGraph) -> Self {
|
||||||
|
let mut resolved_nodes: Vec<PathBuf> = nodes.iter().map(|i| graph.get_node(*i)).collect();
|
||||||
|
resolved_nodes.push(graph.get_node(current_node));
|
||||||
|
CycleError(resolved_nodes)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Display for CycleError {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
let mut disp = String::new();
|
||||||
|
disp.push_str(format!("Include cycle detected:\n{:?} imports ", self.0[0]).as_str());
|
||||||
|
for p in &self.0[1..self.0.len() - 1] {
|
||||||
|
disp.push_str(format!("\n{:?}, which imports ", *p).as_str());
|
||||||
|
}
|
||||||
|
disp.push_str(format!("\n{:?}", self.0[self.0.len() - 1]).as_str());
|
||||||
|
f.write_str(disp.as_str())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<CycleError> for Diagnostic {
|
||||||
|
fn from(e: CycleError) -> Diagnostic {
|
||||||
|
Diagnostic {
|
||||||
|
severity: Some(DiagnosticSeverity::ERROR),
|
||||||
|
range: Range::new(Position::new(0, 0), Position::new(0, 500)),
|
||||||
|
source: Some(consts::SOURCE.into()),
|
||||||
|
message: e.into(),
|
||||||
|
code: None,
|
||||||
|
tags: None,
|
||||||
|
related_information: None,
|
||||||
|
code_description: Option::None,
|
||||||
|
data: Option::None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<CycleError> for String {
|
||||||
|
fn from(e: CycleError) -> String {
|
||||||
|
format!("{}", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod dfs_test {
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
use hamcrest2::prelude::*;
|
||||||
|
use hamcrest2::{assert_that, ok};
|
||||||
|
use petgraph::{algo::is_cyclic_directed, graph::NodeIndex};
|
||||||
|
|
||||||
|
use crate::graph::CachedStableGraph;
|
||||||
|
use crate::{dfs, IncludePosition};
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[logging_macro::log_scope]
|
||||||
|
fn test_graph_dfs() {
|
||||||
|
{
|
||||||
|
let mut graph = CachedStableGraph::new();
|
||||||
|
|
||||||
|
let idx0 = graph.add_node(&PathBuf::from("0"));
|
||||||
|
let idx1 = graph.add_node(&PathBuf::from("1"));
|
||||||
|
let idx2 = graph.add_node(&PathBuf::from("2"));
|
||||||
|
let idx3 = graph.add_node(&PathBuf::from("3"));
|
||||||
|
|
||||||
|
graph.add_edge(idx0, idx1, IncludePosition { line: 2, start: 0, end: 0 });
|
||||||
|
graph.add_edge(idx0, idx2, IncludePosition { line: 3, start: 0, end: 0 });
|
||||||
|
graph.add_edge(idx1, idx3, IncludePosition { line: 5, start: 0, end: 0 });
|
||||||
|
|
||||||
|
let dfs = dfs::Dfs::new(&graph, idx0);
|
||||||
|
|
||||||
|
let mut collection = Vec::new();
|
||||||
|
|
||||||
|
for i in dfs {
|
||||||
|
assert_that!(&i, ok());
|
||||||
|
collection.push(i.unwrap());
|
||||||
|
}
|
||||||
|
|
||||||
|
let nodes: Vec<NodeIndex> = collection.iter().map(|n| n.child).collect();
|
||||||
|
let parents: Vec<Option<NodeIndex>> = collection.iter().map(|n| n.parent).collect();
|
||||||
|
// 0
|
||||||
|
// / \
|
||||||
|
// 1 2
|
||||||
|
// /
|
||||||
|
// 3
|
||||||
|
let expected_nodes = vec![idx0, idx1, idx3, idx2];
|
||||||
|
|
||||||
|
assert_eq!(expected_nodes, nodes);
|
||||||
|
|
||||||
|
let expected_parents = vec![None, Some(idx0), Some(idx1), Some(idx0)];
|
||||||
|
|
||||||
|
assert_eq!(expected_parents, parents);
|
||||||
|
|
||||||
|
assert!(!is_cyclic_directed(&graph.graph));
|
||||||
|
}
|
||||||
|
{
|
||||||
|
let mut graph = CachedStableGraph::new();
|
||||||
|
|
||||||
|
let idx0 = graph.add_node(&PathBuf::from("0"));
|
||||||
|
let idx1 = graph.add_node(&PathBuf::from("1"));
|
||||||
|
let idx2 = graph.add_node(&PathBuf::from("2"));
|
||||||
|
let idx3 = graph.add_node(&PathBuf::from("3"));
|
||||||
|
let idx4 = graph.add_node(&PathBuf::from("4"));
|
||||||
|
let idx5 = graph.add_node(&PathBuf::from("5"));
|
||||||
|
let idx6 = graph.add_node(&PathBuf::from("6"));
|
||||||
|
let idx7 = graph.add_node(&PathBuf::from("7"));
|
||||||
|
|
||||||
|
graph.add_edge(idx0, idx1, IncludePosition { line: 2, start: 0, end: 0 });
|
||||||
|
graph.add_edge(idx0, idx2, IncludePosition { line: 3, start: 0, end: 0 });
|
||||||
|
graph.add_edge(idx1, idx3, IncludePosition { line: 5, start: 0, end: 0 });
|
||||||
|
graph.add_edge(idx1, idx4, IncludePosition { line: 6, start: 0, end: 0 });
|
||||||
|
graph.add_edge(idx2, idx4, IncludePosition { line: 5, start: 0, end: 0 });
|
||||||
|
graph.add_edge(idx2, idx5, IncludePosition { line: 4, start: 0, end: 0 });
|
||||||
|
graph.add_edge(idx3, idx6, IncludePosition { line: 4, start: 0, end: 0 });
|
||||||
|
graph.add_edge(idx4, idx6, IncludePosition { line: 4, start: 0, end: 0 });
|
||||||
|
graph.add_edge(idx6, idx7, IncludePosition { line: 4, start: 0, end: 0 });
|
||||||
|
|
||||||
|
let dfs = dfs::Dfs::new(&graph, idx0);
|
||||||
|
|
||||||
|
let mut collection = Vec::new();
|
||||||
|
|
||||||
|
for i in dfs {
|
||||||
|
assert_that!(&i, ok());
|
||||||
|
collection.push(i.unwrap());
|
||||||
|
}
|
||||||
|
|
||||||
|
let nodes: Vec<NodeIndex> = collection.iter().map(|n| n.child).collect();
|
||||||
|
let parents: Vec<Option<NodeIndex>> = collection.iter().map(|n| n.parent).collect();
|
||||||
|
// 0
|
||||||
|
// / \
|
||||||
|
// 1 2
|
||||||
|
// / \ / \
|
||||||
|
// 3 4 5
|
||||||
|
// \ /
|
||||||
|
// 6 - 7
|
||||||
|
let expected_nodes = vec![idx0, idx1, idx3, idx6, idx7, idx4, idx6, idx7, idx2, idx5, idx4, idx6, idx7];
|
||||||
|
|
||||||
|
assert_eq!(expected_nodes, nodes);
|
||||||
|
|
||||||
|
let expected_parents = vec![
|
||||||
|
None,
|
||||||
|
Some(idx0),
|
||||||
|
Some(idx1),
|
||||||
|
Some(idx3),
|
||||||
|
Some(idx6),
|
||||||
|
Some(idx1),
|
||||||
|
Some(idx4),
|
||||||
|
Some(idx6),
|
||||||
|
Some(idx0),
|
||||||
|
Some(idx2),
|
||||||
|
Some(idx2),
|
||||||
|
Some(idx4),
|
||||||
|
Some(idx6),
|
||||||
|
];
|
||||||
|
|
||||||
|
assert_eq!(expected_parents, parents);
|
||||||
|
|
||||||
|
assert!(!is_cyclic_directed(&graph.graph));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[logging_macro::log_scope]
|
||||||
|
fn test_graph_dfs_cycle() {
|
||||||
|
{
|
||||||
|
let mut graph = CachedStableGraph::new();
|
||||||
|
|
||||||
|
let idx0 = graph.add_node(&PathBuf::from("0"));
|
||||||
|
let idx1 = graph.add_node(&PathBuf::from("1"));
|
||||||
|
let idx2 = graph.add_node(&PathBuf::from("2"));
|
||||||
|
let idx3 = graph.add_node(&PathBuf::from("3"));
|
||||||
|
let idx4 = graph.add_node(&PathBuf::from("4"));
|
||||||
|
let idx5 = graph.add_node(&PathBuf::from("5"));
|
||||||
|
let idx6 = graph.add_node(&PathBuf::from("6"));
|
||||||
|
let idx7 = graph.add_node(&PathBuf::from("7"));
|
||||||
|
|
||||||
|
graph.add_edge(idx0, idx1, IncludePosition { line: 2, start: 0, end: 0 });
|
||||||
|
graph.add_edge(idx0, idx2, IncludePosition { line: 3, start: 0, end: 0 });
|
||||||
|
graph.add_edge(idx1, idx3, IncludePosition { line: 5, start: 0, end: 0 });
|
||||||
|
graph.add_edge(idx1, idx4, IncludePosition { line: 6, start: 0, end: 0 });
|
||||||
|
graph.add_edge(idx2, idx4, IncludePosition { line: 5, start: 0, end: 0 });
|
||||||
|
graph.add_edge(idx2, idx5, IncludePosition { line: 4, start: 0, end: 0 });
|
||||||
|
graph.add_edge(idx3, idx6, IncludePosition { line: 4, start: 0, end: 0 });
|
||||||
|
graph.add_edge(idx4, idx6, IncludePosition { line: 4, start: 0, end: 0 });
|
||||||
|
graph.add_edge(idx6, idx7, IncludePosition { line: 4, start: 0, end: 0 });
|
||||||
|
graph.add_edge(idx7, idx4, IncludePosition { line: 4, start: 0, end: 0 });
|
||||||
|
|
||||||
|
let mut dfs = dfs::Dfs::new(&graph, idx0);
|
||||||
|
|
||||||
|
for _ in 0..5 {
|
||||||
|
if let Some(i) = dfs.next() {
|
||||||
|
assert_that!(&i, ok());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 0
|
||||||
|
// / \
|
||||||
|
// 1 2
|
||||||
|
// / \ / \
|
||||||
|
// 3 4 5
|
||||||
|
// \ / \
|
||||||
|
// 6 - 7
|
||||||
|
|
||||||
|
let next = dfs.next().unwrap();
|
||||||
|
assert_that!(next, err());
|
||||||
|
|
||||||
|
assert!(is_cyclic_directed(&graph.graph));
|
||||||
|
}
|
||||||
|
{
|
||||||
|
let mut graph = CachedStableGraph::new();
|
||||||
|
|
||||||
|
let idx0 = graph.add_node(&PathBuf::from("0"));
|
||||||
|
let idx1 = graph.add_node(&PathBuf::from("1"));
|
||||||
|
|
||||||
|
graph.add_edge(idx0, idx1, IncludePosition { line: 2, start: 0, end: 0 });
|
||||||
|
graph.add_edge(idx1, idx0, IncludePosition { line: 2, start: 0, end: 0 });
|
||||||
|
|
||||||
|
let mut dfs = dfs::Dfs::new(&graph, idx1);
|
||||||
|
|
||||||
|
println!("{:?}", dfs.next());
|
||||||
|
println!("{:?}", dfs.next());
|
||||||
|
println!("{:?}", dfs.next());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
194
server/main/src/diagnostics_parser.rs
Normal file
194
server/main/src/diagnostics_parser.rs
Normal file
|
@ -0,0 +1,194 @@
|
||||||
|
use std::{collections::HashMap, cell::OnceCell, path::Path};
|
||||||
|
|
||||||
|
use regex::Regex;
|
||||||
|
use rust_lsp::lsp_types::{Diagnostic, DiagnosticSeverity, Position, Range};
|
||||||
|
use slog_scope::debug;
|
||||||
|
use url::Url;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
consts,
|
||||||
|
graph::CachedStableGraph,
|
||||||
|
opengl,
|
||||||
|
source_mapper::{SourceMapper, SourceNum},
|
||||||
|
};
|
||||||
|
|
||||||
|
pub struct DiagnosticsParser<'a, T: opengl::ShaderValidator + ?Sized> {
|
||||||
|
line_offset: OnceCell<u32>,
|
||||||
|
line_regex: OnceCell<Regex>,
|
||||||
|
vendor_querier: &'a T,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, T: opengl::ShaderValidator + ?Sized> DiagnosticsParser<'a, T> {
|
||||||
|
pub fn new(vendor_querier: &'a T) -> Self {
|
||||||
|
DiagnosticsParser {
|
||||||
|
line_offset: OnceCell::new(),
|
||||||
|
line_regex: OnceCell::new(),
|
||||||
|
vendor_querier,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_line_regex(&self) -> &Regex {
|
||||||
|
self.line_regex.get_or_init(|| match self.vendor_querier.vendor().as_str() {
|
||||||
|
"NVIDIA Corporation" => {
|
||||||
|
Regex::new(r#"^(?P<filepath>\d+)\((?P<linenum>\d+)\) : (?P<severity>error|warning) [A-C]\d+: (?P<output>.+)"#).unwrap()
|
||||||
|
}
|
||||||
|
_ => Regex::new(r#"^(?P<severity>ERROR|WARNING): (?P<filepath>[^?<>*|"\n]+):(?P<linenum>\d+): (?:'.*' :|[a-z]+\(#\d+\)) +(?P<output>.+)$"#)
|
||||||
|
.unwrap(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_line_offset(&self) -> u32 {
|
||||||
|
*self.line_offset.get_or_init(|| match self.vendor_querier.vendor().as_str() {
|
||||||
|
"ATI Technologies" => 0,
|
||||||
|
_ => 1,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn parse_diagnostics_output(
|
||||||
|
&self, output: String, uri: &Path, source_mapper: &SourceMapper, graph: &CachedStableGraph,
|
||||||
|
) -> HashMap<Url, Vec<Diagnostic>> {
|
||||||
|
let output_lines = output.split('\n').collect::<Vec<&str>>();
|
||||||
|
let mut diagnostics: HashMap<Url, Vec<Diagnostic>> = HashMap::with_capacity(output_lines.len());
|
||||||
|
|
||||||
|
debug!("diagnostics regex selected"; "regex" => self.get_line_regex() .as_str());
|
||||||
|
|
||||||
|
for line in output_lines {
|
||||||
|
let diagnostic_capture = match self.get_line_regex().captures(line) {
|
||||||
|
Some(d) => d,
|
||||||
|
None => continue,
|
||||||
|
};
|
||||||
|
|
||||||
|
debug!("found match for output line"; "line" => line, "capture" => format!("{:?}", diagnostic_capture));
|
||||||
|
|
||||||
|
let msg = diagnostic_capture.name("output").unwrap().as_str();
|
||||||
|
|
||||||
|
let line = match diagnostic_capture.name("linenum") {
|
||||||
|
Some(c) => c.as_str().parse::<u32>().unwrap_or(0),
|
||||||
|
None => 0,
|
||||||
|
} - self.get_line_offset();
|
||||||
|
|
||||||
|
// TODO: line matching maybe
|
||||||
|
/* let line_text = source_lines[line as usize];
|
||||||
|
let leading_whitespace = line_text.len() - line_text.trim_start().len(); */
|
||||||
|
|
||||||
|
let severity = match diagnostic_capture.name("severity") {
|
||||||
|
Some(c) => match c.as_str().to_lowercase().as_str() {
|
||||||
|
"error" => DiagnosticSeverity::ERROR,
|
||||||
|
"warning" => DiagnosticSeverity::WARNING,
|
||||||
|
_ => DiagnosticSeverity::INFORMATION,
|
||||||
|
},
|
||||||
|
_ => DiagnosticSeverity::INFORMATION,
|
||||||
|
};
|
||||||
|
|
||||||
|
let origin = match diagnostic_capture.name("filepath") {
|
||||||
|
Some(o) => {
|
||||||
|
let source_num: SourceNum = o.as_str().parse::<usize>().unwrap().into();
|
||||||
|
let graph_node = source_mapper.get_node(source_num);
|
||||||
|
graph.get_node(graph_node).to_str().unwrap().to_string()
|
||||||
|
}
|
||||||
|
None => uri.to_str().unwrap().to_string(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let diagnostic = Diagnostic {
|
||||||
|
range: Range::new(
|
||||||
|
/* Position::new(line, leading_whitespace as u64),
|
||||||
|
Position::new(line, line_text.len() as u64) */
|
||||||
|
Position::new(line, 0),
|
||||||
|
Position::new(line, 1000),
|
||||||
|
),
|
||||||
|
code: None,
|
||||||
|
severity: Some(severity),
|
||||||
|
source: Some(consts::SOURCE.into()),
|
||||||
|
message: msg.trim().into(),
|
||||||
|
related_information: None,
|
||||||
|
tags: None,
|
||||||
|
code_description: Option::None,
|
||||||
|
data: Option::None,
|
||||||
|
};
|
||||||
|
|
||||||
|
let origin_url = Url::from_file_path(origin).unwrap();
|
||||||
|
match diagnostics.get_mut(&origin_url) {
|
||||||
|
Some(d) => d.push(diagnostic),
|
||||||
|
None => {
|
||||||
|
diagnostics.insert(origin_url, vec![diagnostic]);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
diagnostics
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod diagnostics_test {
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
use slog::slog_o;
|
||||||
|
use url::Url;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
diagnostics_parser::DiagnosticsParser, opengl::MockShaderValidator, source_mapper::SourceMapper, test::new_temp_server,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[logging_macro::log_scope]
|
||||||
|
fn test_nvidia_diagnostics() {
|
||||||
|
slog_scope::scope(&slog_scope::logger().new(slog_o!("driver" => "nvidia")), || {
|
||||||
|
let mut mockgl = MockShaderValidator::new();
|
||||||
|
mockgl.expect_vendor().returning(|| "NVIDIA Corporation".into());
|
||||||
|
let server = new_temp_server(Some(Box::new(mockgl)));
|
||||||
|
|
||||||
|
let output = "0(9) : error C0000: syntax error, unexpected '}', expecting ',' or ';' at token \"}\"";
|
||||||
|
|
||||||
|
#[cfg(target_family = "unix")]
|
||||||
|
let path: PathBuf = "/home/noah/.minecraft/shaderpacks/test/shaders/final.fsh".into();
|
||||||
|
#[cfg(target_family = "windows")]
|
||||||
|
let path: PathBuf = "c:\\home\\noah\\.minecraft\\shaderpacks\\test\\shaders\\final.fsh".into();
|
||||||
|
|
||||||
|
let mut source_mapper = SourceMapper::new(0);
|
||||||
|
source_mapper.get_num(server.graph.borrow_mut().add_node(&path));
|
||||||
|
|
||||||
|
let parser = DiagnosticsParser::new(server.opengl_context.as_ref());
|
||||||
|
|
||||||
|
let results =
|
||||||
|
parser.parse_diagnostics_output(output.to_string(), path.parent().unwrap(), &source_mapper, &server.graph.borrow());
|
||||||
|
|
||||||
|
assert_eq!(results.len(), 1);
|
||||||
|
let first = results.into_iter().next().unwrap();
|
||||||
|
assert_eq!(first.0, Url::from_file_path(path).unwrap());
|
||||||
|
server.endpoint.request_shutdown();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[logging_macro::log_scope]
|
||||||
|
fn test_amd_diagnostics() {
|
||||||
|
slog_scope::scope(&slog_scope::logger().new(slog_o!("driver" => "amd")), || {
|
||||||
|
let mut mockgl = MockShaderValidator::new();
|
||||||
|
mockgl.expect_vendor().returning(|| "ATI Technologies".into());
|
||||||
|
let server = new_temp_server(Some(Box::new(mockgl)));
|
||||||
|
|
||||||
|
let output = "ERROR: 0:1: '' : syntax error: #line
|
||||||
|
ERROR: 0:10: '' : syntax error: #line
|
||||||
|
ERROR: 0:15: 'varying' : syntax error: syntax error
|
||||||
|
";
|
||||||
|
|
||||||
|
#[cfg(target_family = "unix")]
|
||||||
|
let path: PathBuf = "/home/noah/.minecraft/shaderpacks/test/shaders/final.fsh".into();
|
||||||
|
#[cfg(target_family = "windows")]
|
||||||
|
let path: PathBuf = "c:\\home\\noah\\.minecraft\\shaderpacks\\test\\shaders\\final.fsh".into();
|
||||||
|
|
||||||
|
let mut source_mapper = SourceMapper::new(0);
|
||||||
|
source_mapper.get_num(server.graph.borrow_mut().add_node(&path));
|
||||||
|
|
||||||
|
let parser = DiagnosticsParser::new(server.opengl_context.as_ref());
|
||||||
|
|
||||||
|
let results =
|
||||||
|
parser.parse_diagnostics_output(output.to_string(), path.parent().unwrap(), &source_mapper, &server.graph.borrow());
|
||||||
|
|
||||||
|
assert_eq!(results.len(), 1);
|
||||||
|
let first = results.into_iter().next().unwrap();
|
||||||
|
assert_eq!(first.1.len(), 3);
|
||||||
|
server.endpoint.request_shutdown();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
374
server/main/src/graph.rs
Normal file
374
server/main/src/graph.rs
Normal file
|
@ -0,0 +1,374 @@
|
||||||
|
use petgraph::stable_graph::EdgeIndex;
|
||||||
|
use petgraph::stable_graph::NodeIndex;
|
||||||
|
use petgraph::stable_graph::StableDiGraph;
|
||||||
|
use petgraph::visit::EdgeRef;
|
||||||
|
use petgraph::Direction;
|
||||||
|
|
||||||
|
use std::{
|
||||||
|
collections::{HashMap, HashSet},
|
||||||
|
path::{Path, PathBuf},
|
||||||
|
str::FromStr,
|
||||||
|
};
|
||||||
|
|
||||||
|
use super::IncludePosition;
|
||||||
|
|
||||||
|
/// Wraps a `StableDiGraph` with caching behaviour for node search by maintaining
|
||||||
|
/// an index for node value to node index and a reverse index.
|
||||||
|
/// This allows for **O(1)** lookup for a value if it exists, else **O(n)**.
|
||||||
|
pub struct CachedStableGraph {
|
||||||
|
// StableDiGraph is used as it allows for String node values, essential for
|
||||||
|
// generating the GraphViz DOT render.
|
||||||
|
pub graph: StableDiGraph<String, IncludePosition>,
|
||||||
|
cache: HashMap<PathBuf, NodeIndex>,
|
||||||
|
// Maps a node index to its abstracted string representation.
|
||||||
|
// Mainly used as the graph is based on NodeIndex.
|
||||||
|
reverse_index: HashMap<NodeIndex, PathBuf>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CachedStableGraph {
|
||||||
|
#[allow(clippy::new_without_default)]
|
||||||
|
pub fn new() -> CachedStableGraph {
|
||||||
|
CachedStableGraph {
|
||||||
|
graph: StableDiGraph::new(),
|
||||||
|
cache: HashMap::new(),
|
||||||
|
reverse_index: HashMap::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the `NodeIndex` for a given graph node with the value of `name`
|
||||||
|
/// and caches the result in the `HashMap`. Complexity is **O(1)** if the value
|
||||||
|
/// is cached (which should always be the case), else **O(n)** where **n** is
|
||||||
|
/// the number of node indices, as an exhaustive search must be done.
|
||||||
|
pub fn find_node(&mut self, name: &Path) -> Option<NodeIndex> {
|
||||||
|
match self.cache.get(name) {
|
||||||
|
Some(n) => Some(*n),
|
||||||
|
None => {
|
||||||
|
// If the string is not in cache, O(n) search the graph (i know...) and then cache the NodeIndex
|
||||||
|
// for later
|
||||||
|
let n = self.graph.node_indices().find(|n| self.graph[*n] == name.to_str().unwrap());
|
||||||
|
if let Some(n) = n {
|
||||||
|
self.cache.insert(name.into(), n);
|
||||||
|
}
|
||||||
|
n
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns the `PathBuf` for a given `NodeIndex`
|
||||||
|
pub fn get_node(&self, node: NodeIndex) -> PathBuf {
|
||||||
|
PathBuf::from_str(&self.graph[node]).unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns an iterator over all the `IncludePosition`'s between a parent and its child for all the positions
|
||||||
|
/// that the child may be imported into the parent, in order of import.
|
||||||
|
pub fn get_child_positions(&self, parent: NodeIndex, child: NodeIndex) -> impl Iterator<Item = IncludePosition> + '_ {
|
||||||
|
let mut edges = self
|
||||||
|
.graph
|
||||||
|
.edges(parent)
|
||||||
|
.filter_map(move |edge| {
|
||||||
|
let target = self.graph.edge_endpoints(edge.id()).unwrap().1;
|
||||||
|
if target != child {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
Some(self.graph[edge.id()])
|
||||||
|
})
|
||||||
|
.collect::<Vec<IncludePosition>>();
|
||||||
|
edges.sort_by(|x, y| x.line.cmp(&y.line));
|
||||||
|
edges.into_iter()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns an iterator over all the `(NodeIndex, IncludePosition)` tuples between a node and all its children, in order
|
||||||
|
/// of import.
|
||||||
|
pub fn get_all_child_positions(&self, node: NodeIndex) -> impl Iterator<Item = (NodeIndex, IncludePosition)> + '_ {
|
||||||
|
let mut edges = self.graph.edges(node).map(|edge| {
|
||||||
|
let child = self.graph.edge_endpoints(edge.id()).unwrap().1;
|
||||||
|
(child, self.graph[edge.id()])
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
edges.sort_by(|x, y| x.1.line.cmp(&y.1.line));
|
||||||
|
edges.into_iter()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn add_node(&mut self, name: &Path) -> NodeIndex {
|
||||||
|
if let Some(idx) = self.cache.get(name) {
|
||||||
|
return *idx;
|
||||||
|
}
|
||||||
|
let idx = self.graph.add_node(name.to_str().unwrap().to_string());
|
||||||
|
self.cache.insert(name.to_owned(), idx);
|
||||||
|
self.reverse_index.insert(idx, name.to_owned());
|
||||||
|
idx
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn add_edge(&mut self, parent: NodeIndex, child: NodeIndex, meta: IncludePosition) -> EdgeIndex {
|
||||||
|
self.graph.add_edge(parent, child, meta)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn remove_edge(&mut self, parent: NodeIndex, child: NodeIndex, position: IncludePosition) {
|
||||||
|
self.graph
|
||||||
|
.edges(parent)
|
||||||
|
.find(|edge| self.graph.edge_endpoints(edge.id()).unwrap().1 == child && *edge.weight() == position)
|
||||||
|
.map(|edge| edge.id())
|
||||||
|
.and_then(|edge| self.graph.remove_edge(edge));
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn child_node_indexes(&self, node: NodeIndex) -> impl Iterator<Item = NodeIndex> + '_ {
|
||||||
|
self.graph.neighbors(node)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn collect_root_ancestors(&self, node: NodeIndex) -> Vec<NodeIndex> {
|
||||||
|
let mut visited = HashSet::new();
|
||||||
|
self.get_root_ancestors(node, node, &mut visited)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: impl Iterator
|
||||||
|
fn parent_node_indexes(&self, node: NodeIndex) -> Vec<NodeIndex> {
|
||||||
|
self.graph.neighbors_directed(node, Direction::Incoming).collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_root_ancestors(&self, initial: NodeIndex, node: NodeIndex, visited: &mut HashSet<NodeIndex>) -> Vec<NodeIndex> {
|
||||||
|
if node == initial && !visited.is_empty() {
|
||||||
|
return vec![];
|
||||||
|
}
|
||||||
|
|
||||||
|
let parents = self.parent_node_indexes(node);
|
||||||
|
let mut collection = Vec::with_capacity(parents.len());
|
||||||
|
|
||||||
|
for ancestor in &parents {
|
||||||
|
visited.insert(*ancestor);
|
||||||
|
}
|
||||||
|
|
||||||
|
for ancestor in &parents {
|
||||||
|
let ancestors = self.parent_node_indexes(*ancestor);
|
||||||
|
if !ancestors.is_empty() {
|
||||||
|
collection.extend(self.get_root_ancestors(initial, *ancestor, visited));
|
||||||
|
} else {
|
||||||
|
collection.push(*ancestor);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
collection
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
impl CachedStableGraph {
|
||||||
|
fn parent_node_names(&self, node: NodeIndex) -> Vec<PathBuf> {
|
||||||
|
self.graph
|
||||||
|
.neighbors_directed(node, Direction::Incoming)
|
||||||
|
.map(|n| self.reverse_index.get(&n).unwrap().clone())
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn child_node_names(&self, node: NodeIndex) -> Vec<PathBuf> {
|
||||||
|
self.graph
|
||||||
|
.neighbors(node)
|
||||||
|
.map(|n| self.reverse_index.get(&n).unwrap().clone())
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn remove_node(&mut self, name: &Path) {
|
||||||
|
let idx = self.cache.remove(name);
|
||||||
|
if let Some(idx) = idx {
|
||||||
|
self.graph.remove_node(idx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod graph_test {
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
use petgraph::graph::NodeIndex;
|
||||||
|
|
||||||
|
use crate::{graph::CachedStableGraph, IncludePosition};
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[logging_macro::log_scope]
|
||||||
|
fn test_graph_two_connected_nodes() {
|
||||||
|
let mut graph = CachedStableGraph::new();
|
||||||
|
|
||||||
|
let idx1 = graph.add_node(&PathBuf::from("sample"));
|
||||||
|
let idx2 = graph.add_node(&PathBuf::from("banana"));
|
||||||
|
graph.add_edge(idx1, idx2, IncludePosition { line: 3, start: 0, end: 0 });
|
||||||
|
|
||||||
|
let children = graph.child_node_names(idx1);
|
||||||
|
assert_eq!(children.len(), 1);
|
||||||
|
assert_eq!(children[0], Into::<PathBuf>::into("banana".to_string()));
|
||||||
|
|
||||||
|
let children: Vec<NodeIndex> = graph.child_node_indexes(idx1).collect();
|
||||||
|
assert_eq!(children.len(), 1);
|
||||||
|
assert_eq!(children[0], idx2);
|
||||||
|
|
||||||
|
let parents = graph.parent_node_names(idx1);
|
||||||
|
assert_eq!(parents.len(), 0);
|
||||||
|
|
||||||
|
let parents = graph.parent_node_names(idx2);
|
||||||
|
assert_eq!(parents.len(), 1);
|
||||||
|
assert_eq!(parents[0], Into::<PathBuf>::into("sample".to_string()));
|
||||||
|
|
||||||
|
let parents = graph.parent_node_indexes(idx2);
|
||||||
|
assert_eq!(parents.len(), 1);
|
||||||
|
assert_eq!(parents[0], idx1);
|
||||||
|
|
||||||
|
let ancestors = graph.collect_root_ancestors(idx2);
|
||||||
|
assert_eq!(ancestors.len(), 1);
|
||||||
|
assert_eq!(ancestors[0], idx1);
|
||||||
|
|
||||||
|
let ancestors = graph.collect_root_ancestors(idx1);
|
||||||
|
assert_eq!(ancestors.len(), 0);
|
||||||
|
|
||||||
|
graph.remove_node(&PathBuf::from("sample"));
|
||||||
|
assert_eq!(graph.graph.node_count(), 1);
|
||||||
|
assert!(graph.find_node(&PathBuf::from("sample")).is_none());
|
||||||
|
|
||||||
|
let neighbors = graph.child_node_names(idx2);
|
||||||
|
assert_eq!(neighbors.len(), 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[logging_macro::log_scope]
|
||||||
|
fn test_double_import() {
|
||||||
|
let mut graph = CachedStableGraph::new();
|
||||||
|
|
||||||
|
let idx0 = graph.add_node(&PathBuf::from("0"));
|
||||||
|
let idx1 = graph.add_node(&PathBuf::from("1"));
|
||||||
|
|
||||||
|
graph.add_edge(idx0, idx1, IncludePosition { line: 2, start: 0, end: 0 });
|
||||||
|
graph.add_edge(idx0, idx1, IncludePosition { line: 4, start: 0, end: 0 });
|
||||||
|
|
||||||
|
// 0
|
||||||
|
// / \
|
||||||
|
// 1 1
|
||||||
|
|
||||||
|
assert_eq!(2, graph.get_child_positions(idx0, idx1).count());
|
||||||
|
|
||||||
|
let mut edge_metas = graph.get_child_positions(idx0, idx1);
|
||||||
|
assert_eq!(Some(IncludePosition { line: 2, start: 0, end: 0 }), edge_metas.next());
|
||||||
|
assert_eq!(Some(IncludePosition { line: 4, start: 0, end: 0 }), edge_metas.next());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[logging_macro::log_scope]
|
||||||
|
fn test_collect_root_ancestors() {
|
||||||
|
{
|
||||||
|
let mut graph = CachedStableGraph::new();
|
||||||
|
|
||||||
|
let idx0 = graph.add_node(&PathBuf::from("0"));
|
||||||
|
let idx1 = graph.add_node(&PathBuf::from("1"));
|
||||||
|
let idx2 = graph.add_node(&PathBuf::from("2"));
|
||||||
|
let idx3 = graph.add_node(&PathBuf::from("3"));
|
||||||
|
|
||||||
|
graph.add_edge(idx0, idx1, IncludePosition { line: 2, start: 0, end: 0 });
|
||||||
|
graph.add_edge(idx1, idx2, IncludePosition { line: 3, start: 0, end: 0 });
|
||||||
|
graph.add_edge(idx3, idx1, IncludePosition { line: 4, start: 0, end: 0 });
|
||||||
|
|
||||||
|
// 0 3
|
||||||
|
// |/
|
||||||
|
// 1
|
||||||
|
// |
|
||||||
|
// 2
|
||||||
|
|
||||||
|
let roots = graph.collect_root_ancestors(idx2);
|
||||||
|
assert_eq!(roots, vec![idx3, idx0]);
|
||||||
|
|
||||||
|
let roots = graph.collect_root_ancestors(idx1);
|
||||||
|
assert_eq!(roots, vec![idx3, idx0]);
|
||||||
|
|
||||||
|
let roots = graph.collect_root_ancestors(idx0);
|
||||||
|
assert_eq!(roots, vec![]);
|
||||||
|
|
||||||
|
let roots = graph.collect_root_ancestors(idx3);
|
||||||
|
assert_eq!(roots, vec![]);
|
||||||
|
}
|
||||||
|
{
|
||||||
|
let mut graph = CachedStableGraph::new();
|
||||||
|
|
||||||
|
let idx0 = graph.add_node(&PathBuf::from("0"));
|
||||||
|
let idx1 = graph.add_node(&PathBuf::from("1"));
|
||||||
|
let idx2 = graph.add_node(&PathBuf::from("2"));
|
||||||
|
let idx3 = graph.add_node(&PathBuf::from("3"));
|
||||||
|
|
||||||
|
graph.add_edge(idx0, idx1, IncludePosition { line: 2, start: 0, end: 0 });
|
||||||
|
graph.add_edge(idx0, idx2, IncludePosition { line: 3, start: 0, end: 0 });
|
||||||
|
graph.add_edge(idx1, idx3, IncludePosition { line: 5, start: 0, end: 0 });
|
||||||
|
|
||||||
|
// 0
|
||||||
|
// / \
|
||||||
|
// 1 2
|
||||||
|
// /
|
||||||
|
// 3
|
||||||
|
|
||||||
|
let roots = graph.collect_root_ancestors(idx3);
|
||||||
|
assert_eq!(roots, vec![idx0]);
|
||||||
|
|
||||||
|
let roots = graph.collect_root_ancestors(idx2);
|
||||||
|
assert_eq!(roots, vec![idx0]);
|
||||||
|
|
||||||
|
let roots = graph.collect_root_ancestors(idx1);
|
||||||
|
assert_eq!(roots, vec![idx0]);
|
||||||
|
|
||||||
|
let roots = graph.collect_root_ancestors(idx0);
|
||||||
|
assert_eq!(roots, vec![]);
|
||||||
|
}
|
||||||
|
{
|
||||||
|
let mut graph = CachedStableGraph::new();
|
||||||
|
|
||||||
|
let idx0 = graph.add_node(&PathBuf::from("0"));
|
||||||
|
let idx1 = graph.add_node(&PathBuf::from("1"));
|
||||||
|
let idx2 = graph.add_node(&PathBuf::from("2"));
|
||||||
|
let idx3 = graph.add_node(&PathBuf::from("3"));
|
||||||
|
|
||||||
|
graph.add_edge(idx0, idx1, IncludePosition { line: 2, start: 0, end: 0 });
|
||||||
|
graph.add_edge(idx2, idx3, IncludePosition { line: 3, start: 0, end: 0 });
|
||||||
|
graph.add_edge(idx1, idx3, IncludePosition { line: 5, start: 0, end: 0 });
|
||||||
|
|
||||||
|
// 0
|
||||||
|
// \
|
||||||
|
// 2 1
|
||||||
|
// \ /
|
||||||
|
// 3
|
||||||
|
|
||||||
|
let roots = graph.collect_root_ancestors(idx3);
|
||||||
|
assert_eq!(roots, vec![idx0, idx2]);
|
||||||
|
|
||||||
|
let roots = graph.collect_root_ancestors(idx2);
|
||||||
|
assert_eq!(roots, vec![]);
|
||||||
|
|
||||||
|
let roots = graph.collect_root_ancestors(idx1);
|
||||||
|
assert_eq!(roots, vec![idx0]);
|
||||||
|
|
||||||
|
let roots = graph.collect_root_ancestors(idx0);
|
||||||
|
assert_eq!(roots, vec![]);
|
||||||
|
}
|
||||||
|
{
|
||||||
|
let mut graph = CachedStableGraph::new();
|
||||||
|
|
||||||
|
let idx0 = graph.add_node(&PathBuf::from("0"));
|
||||||
|
let idx1 = graph.add_node(&PathBuf::from("1"));
|
||||||
|
let idx2 = graph.add_node(&PathBuf::from("2"));
|
||||||
|
let idx3 = graph.add_node(&PathBuf::from("3"));
|
||||||
|
|
||||||
|
graph.add_edge(idx0, idx1, IncludePosition { line: 2, start: 0, end: 0 });
|
||||||
|
graph.add_edge(idx1, idx2, IncludePosition { line: 4, start: 0, end: 0 });
|
||||||
|
graph.add_edge(idx1, idx3, IncludePosition { line: 6, start: 0, end: 0 });
|
||||||
|
|
||||||
|
// 0
|
||||||
|
// |
|
||||||
|
// 1
|
||||||
|
// / \
|
||||||
|
// 2 3
|
||||||
|
|
||||||
|
let roots = graph.collect_root_ancestors(idx3);
|
||||||
|
assert_eq!(roots, vec![idx0]);
|
||||||
|
|
||||||
|
let roots = graph.collect_root_ancestors(idx2);
|
||||||
|
assert_eq!(roots, vec![idx0]);
|
||||||
|
|
||||||
|
let roots = graph.collect_root_ancestors(idx1);
|
||||||
|
assert_eq!(roots, vec![idx0]);
|
||||||
|
|
||||||
|
let roots = graph.collect_root_ancestors(idx0);
|
||||||
|
assert_eq!(roots, vec![]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
80
server/main/src/linemap.rs
Normal file
80
server/main/src/linemap.rs
Normal file
|
@ -0,0 +1,80 @@
|
||||||
|
use rust_lsp::lsp_types::Position;
|
||||||
|
|
||||||
|
pub struct LineMap {
|
||||||
|
positions: Vec<usize>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl LineMap {
|
||||||
|
pub fn new(source: &str) -> Self {
|
||||||
|
let mut positions = vec![0];
|
||||||
|
for (i, char) in source.char_indices() {
|
||||||
|
if char == '\n' {
|
||||||
|
positions.push(i + 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
LineMap { positions }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn offset_for_position(&self, position: Position) -> usize {
|
||||||
|
self.positions[position.line as usize] + (position.character as usize)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod test {
|
||||||
|
use rust_lsp::lsp_types::Position;
|
||||||
|
|
||||||
|
use crate::linemap::LineMap;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[logging_macro::log_scope]
|
||||||
|
fn test_linemap() {
|
||||||
|
struct Test {
|
||||||
|
string: &'static str,
|
||||||
|
pos: Position,
|
||||||
|
offset: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
let cases = vec![
|
||||||
|
Test {
|
||||||
|
string: "sample\ntext",
|
||||||
|
pos: Position { line: 1, character: 2 },
|
||||||
|
offset: 9,
|
||||||
|
},
|
||||||
|
Test {
|
||||||
|
string: "banana",
|
||||||
|
pos: Position { line: 0, character: 0 },
|
||||||
|
offset: 0,
|
||||||
|
},
|
||||||
|
Test {
|
||||||
|
string: "banana",
|
||||||
|
pos: Position { line: 0, character: 1 },
|
||||||
|
offset: 1,
|
||||||
|
},
|
||||||
|
Test {
|
||||||
|
string: "sample\ntext",
|
||||||
|
pos: Position { line: 1, character: 0 },
|
||||||
|
offset: 7,
|
||||||
|
},
|
||||||
|
Test {
|
||||||
|
string: "sample\n\ttext",
|
||||||
|
pos: Position { line: 1, character: 2 },
|
||||||
|
offset: 9,
|
||||||
|
},
|
||||||
|
Test {
|
||||||
|
string: "sample\r\ntext",
|
||||||
|
pos: Position { line: 1, character: 0 },
|
||||||
|
offset: 8,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
for case in cases {
|
||||||
|
let linemap = LineMap::new(case.string);
|
||||||
|
|
||||||
|
let offset = linemap.offset_for_position(case.pos);
|
||||||
|
|
||||||
|
assert_eq!(offset, case.offset, "{:?}", case.string);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
959
server/main/src/main.rs
Normal file
959
server/main/src/main.rs
Normal file
|
@ -0,0 +1,959 @@
|
||||||
|
#![feature(once_cell)]
|
||||||
|
#![feature(option_get_or_insert_default)]
|
||||||
|
|
||||||
|
use merge_views::FilialTuple;
|
||||||
|
use rust_lsp::jsonrpc::{method_types::*, *};
|
||||||
|
use rust_lsp::lsp::*;
|
||||||
|
use rust_lsp::lsp_types::{notification::*, *};
|
||||||
|
|
||||||
|
use petgraph::stable_graph::NodeIndex;
|
||||||
|
use path_slash::PathExt;
|
||||||
|
|
||||||
|
use serde::Deserialize;
|
||||||
|
use serde_json::{from_value, Value};
|
||||||
|
|
||||||
|
use tree_sitter::Parser;
|
||||||
|
use url_norm::FromUrl;
|
||||||
|
|
||||||
|
use walkdir::WalkDir;
|
||||||
|
|
||||||
|
use std::collections::{HashMap, HashSet};
|
||||||
|
use std::convert::TryFrom;
|
||||||
|
use std::fmt::{Debug, Display, Formatter};
|
||||||
|
use std::fs;
|
||||||
|
use std::io::{stdin, stdout, BufRead, BufReader};
|
||||||
|
use std::iter::{Extend, FromIterator};
|
||||||
|
use std::rc::Rc;
|
||||||
|
use std::str::FromStr;
|
||||||
|
|
||||||
|
use std::{
|
||||||
|
cell::RefCell,
|
||||||
|
path::{Path, PathBuf},
|
||||||
|
};
|
||||||
|
|
||||||
|
use slog::Level;
|
||||||
|
use slog_scope::{debug, error, info, warn};
|
||||||
|
|
||||||
|
use path_slash::PathBufExt;
|
||||||
|
|
||||||
|
use anyhow::{anyhow, Result};
|
||||||
|
|
||||||
|
use regex::Regex;
|
||||||
|
|
||||||
|
use lazy_static::lazy_static;
|
||||||
|
|
||||||
|
mod commands;
|
||||||
|
mod configuration;
|
||||||
|
mod consts;
|
||||||
|
mod dfs;
|
||||||
|
mod diagnostics_parser;
|
||||||
|
mod graph;
|
||||||
|
mod linemap;
|
||||||
|
mod lsp_ext;
|
||||||
|
mod merge_views;
|
||||||
|
mod navigation;
|
||||||
|
mod opengl;
|
||||||
|
mod source_mapper;
|
||||||
|
mod url_norm;
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod test;
|
||||||
|
|
||||||
|
pub fn is_top_level(path: &Path) -> bool {
|
||||||
|
let path = path.to_slash().unwrap();
|
||||||
|
if !RE_WORLD_FOLDER.is_match(&path) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
let parts: Vec<&str> = path.split("/").collect();
|
||||||
|
let len = parts.len();
|
||||||
|
(len == 3 || len == 2) && TOPLEVEL_FILES.contains(parts[len - 1])
|
||||||
|
}
|
||||||
|
|
||||||
|
lazy_static! {
|
||||||
|
static ref RE_INCLUDE: Regex = Regex::new(r#"^(?:\s)*?(?:#include) "(.+)"\r?"#).unwrap();
|
||||||
|
static ref RE_WORLD_FOLDER: Regex = Regex::new(r#"^shaders(/world-?\d+)?"#).unwrap();
|
||||||
|
static ref TOPLEVEL_FILES: HashSet<String> = {
|
||||||
|
let mut set = HashSet::with_capacity(1716);
|
||||||
|
for ext in ["fsh", "vsh", "gsh", "csh"] {
|
||||||
|
set.insert(format!("composite.{}", ext));
|
||||||
|
set.insert(format!("deferred.{}", ext));
|
||||||
|
set.insert(format!("prepare.{}", ext));
|
||||||
|
set.insert(format!("shadowcomp.{}", ext));
|
||||||
|
for i in 1..=99 {
|
||||||
|
set.insert(format!("composite{}.{}", i, ext));
|
||||||
|
set.insert(format!("deferred{}.{}", i, ext));
|
||||||
|
set.insert(format!("prepare{}.{}", i, ext));
|
||||||
|
set.insert(format!("shadowcomp{}.{}", i, ext));
|
||||||
|
}
|
||||||
|
set.insert(format!("composite_pre.{}", ext));
|
||||||
|
set.insert(format!("deferred_pre.{}", ext));
|
||||||
|
set.insert(format!("final.{}", ext));
|
||||||
|
set.insert(format!("gbuffers_armor_glint.{}", ext));
|
||||||
|
set.insert(format!("gbuffers_basic.{}", ext));
|
||||||
|
set.insert(format!("gbuffers_beaconbeam.{}", ext));
|
||||||
|
set.insert(format!("gbuffers_block.{}", ext));
|
||||||
|
set.insert(format!("gbuffers_clouds.{}", ext));
|
||||||
|
set.insert(format!("gbuffers_damagedblock.{}", ext));
|
||||||
|
set.insert(format!("gbuffers_entities.{}", ext));
|
||||||
|
set.insert(format!("gbuffers_entities_glowing.{}", ext));
|
||||||
|
set.insert(format!("gbuffers_hand.{}", ext));
|
||||||
|
set.insert(format!("gbuffers_hand_water.{}", ext));
|
||||||
|
set.insert(format!("gbuffers_item.{}", ext));
|
||||||
|
set.insert(format!("gbuffers_line.{}", ext));
|
||||||
|
set.insert(format!("gbuffers_skybasic.{}", ext));
|
||||||
|
set.insert(format!("gbuffers_skytextured.{}", ext));
|
||||||
|
set.insert(format!("gbuffers_spidereyes.{}", ext));
|
||||||
|
set.insert(format!("gbuffers_terrain.{}", ext));
|
||||||
|
set.insert(format!("gbuffers_terrain_cutout.{}", ext));
|
||||||
|
set.insert(format!("gbuffers_terrain_cutout_mip.{}", ext));
|
||||||
|
set.insert(format!("gbuffers_terrain_solid.{}", ext));
|
||||||
|
set.insert(format!("gbuffers_textured.{}", ext));
|
||||||
|
set.insert(format!("gbuffers_textured_lit.{}", ext));
|
||||||
|
set.insert(format!("gbuffers_water.{}", ext));
|
||||||
|
set.insert(format!("gbuffers_weather.{}", ext));
|
||||||
|
set.insert(format!("shadow.{}", ext));
|
||||||
|
set.insert(format!("shadow_cutout.{}", ext));
|
||||||
|
set.insert(format!("shadow_solid.{}", ext));
|
||||||
|
}
|
||||||
|
let base_char_num = 'a' as u8;
|
||||||
|
for suffix_num in 0u8..=25u8 {
|
||||||
|
let suffix_char = (base_char_num + suffix_num) as char;
|
||||||
|
set.insert(format!("composite_{}.csh", suffix_char));
|
||||||
|
set.insert(format!("deferred_{}.csh", suffix_char));
|
||||||
|
set.insert(format!("prepare_{}.csh", suffix_char));
|
||||||
|
set.insert(format!("shadowcomp_{}.csh", suffix_char));
|
||||||
|
for i in 1..=99 {
|
||||||
|
let total_suffix = format!("{}_{}", i, suffix_char);
|
||||||
|
set.insert(format!("composite{}.csh", total_suffix));
|
||||||
|
set.insert(format!("deferred{}.csh", total_suffix));
|
||||||
|
set.insert(format!("prepare{}.csh", total_suffix));
|
||||||
|
set.insert(format!("shadowcomp{}.csh", total_suffix));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
set
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
let guard = logging::set_logger_with_level(Level::Info);
|
||||||
|
|
||||||
|
let endpoint_output = LSPEndpoint::create_lsp_output_with_output_stream(stdout);
|
||||||
|
|
||||||
|
let cache_graph = graph::CachedStableGraph::new();
|
||||||
|
|
||||||
|
let mut parser = Parser::new();
|
||||||
|
parser.set_language(tree_sitter_glsl::language()).unwrap();
|
||||||
|
|
||||||
|
let mut langserver = MinecraftShaderLanguageServer {
|
||||||
|
endpoint: endpoint_output.clone(),
|
||||||
|
graph: Rc::new(RefCell::new(cache_graph)),
|
||||||
|
root: "".into(),
|
||||||
|
command_provider: None,
|
||||||
|
opengl_context: Rc::new(opengl::OpenGlContext::new()),
|
||||||
|
tree_sitter: Rc::new(RefCell::new(parser)),
|
||||||
|
log_guard: Some(guard),
|
||||||
|
};
|
||||||
|
|
||||||
|
langserver.command_provider = Some(commands::CustomCommandProvider::new(vec![
|
||||||
|
(
|
||||||
|
"graphDot",
|
||||||
|
Box::new(commands::graph_dot::GraphDotCommand {
|
||||||
|
graph: langserver.graph.clone(),
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"virtualMerge",
|
||||||
|
Box::new(commands::merged_includes::VirtualMergedDocument {
|
||||||
|
graph: langserver.graph.clone(),
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"parseTree",
|
||||||
|
Box::new(commands::parse_tree::TreeSitterSExpr {
|
||||||
|
tree_sitter: langserver.tree_sitter.clone(),
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
]));
|
||||||
|
|
||||||
|
LSPEndpoint::run_server_from_input(&mut stdin().lock(), endpoint_output, langserver);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct MinecraftShaderLanguageServer {
|
||||||
|
endpoint: Endpoint,
|
||||||
|
graph: Rc<RefCell<graph::CachedStableGraph>>,
|
||||||
|
root: PathBuf,
|
||||||
|
command_provider: Option<commands::CustomCommandProvider>,
|
||||||
|
opengl_context: Rc<dyn opengl::ShaderValidator>,
|
||||||
|
tree_sitter: Rc<RefCell<Parser>>,
|
||||||
|
log_guard: Option<slog_scope::GlobalLoggerGuard>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, PartialEq, Eq, Hash)]
|
||||||
|
pub struct IncludePosition {
|
||||||
|
// the 0-indexed line on which the include lives.
|
||||||
|
line: usize,
|
||||||
|
// the 0-indexed char offset defining the start of the include path string.
|
||||||
|
start: usize,
|
||||||
|
// the 0-indexed char offset defining the end of the include path string.
|
||||||
|
end: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Debug for IncludePosition {
|
||||||
|
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||||
|
write!(f, "{{line: {}}}", self.line)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Display for IncludePosition {
|
||||||
|
fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), std::fmt::Error> {
|
||||||
|
write!(f, "{{line: {}}}", self.line)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum TreeType {
|
||||||
|
Fragment,
|
||||||
|
Vertex,
|
||||||
|
Geometry,
|
||||||
|
Compute,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MinecraftShaderLanguageServer {
|
||||||
|
pub fn error_not_available<DATA>(data: DATA) -> MethodError<DATA> {
|
||||||
|
let msg = "Functionality not implemented.".to_string();
|
||||||
|
MethodError::<DATA> {
|
||||||
|
code: 1,
|
||||||
|
message: msg,
|
||||||
|
data,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn build_initial_graph(&self) {
|
||||||
|
info!("generating graph for current root"; "root" => self.root.to_str().unwrap());
|
||||||
|
|
||||||
|
// filter directories and files not ending in any of the 3 extensions
|
||||||
|
WalkDir::new(&self.root)
|
||||||
|
.into_iter()
|
||||||
|
.filter_map(|entry| {
|
||||||
|
if entry.is_err() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
let entry = entry.unwrap();
|
||||||
|
let path = entry.path();
|
||||||
|
if path.is_dir() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
let ext = match path.extension() {
|
||||||
|
Some(e) => e,
|
||||||
|
None => return None,
|
||||||
|
};
|
||||||
|
|
||||||
|
// TODO: include user added extensions with a set
|
||||||
|
if ext != "vsh" && ext != "fsh" && ext != "csh" && ext != "gsh" && ext != "glsl" && ext != "inc" {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
Some(entry.into_path())
|
||||||
|
})
|
||||||
|
.for_each(|path| {
|
||||||
|
// iterate all valid found files, search for includes, add a node into the graph for each
|
||||||
|
// file and add a file->includes KV into the map
|
||||||
|
self.add_file_and_includes_to_graph(&path);
|
||||||
|
});
|
||||||
|
|
||||||
|
info!("finished building project include graph");
|
||||||
|
}
|
||||||
|
|
||||||
|
fn add_file_and_includes_to_graph(&self, path: &Path) {
|
||||||
|
let includes = self.find_includes(path);
|
||||||
|
|
||||||
|
let idx = self.graph.borrow_mut().add_node(path);
|
||||||
|
|
||||||
|
debug!("adding includes for new file"; "file" => path.to_str().unwrap(), "includes" => format!("{:?}", includes));
|
||||||
|
for include in includes {
|
||||||
|
self.add_include(include, idx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn add_include(&self, include: (PathBuf, IncludePosition), node: NodeIndex) {
|
||||||
|
let child = self.graph.borrow_mut().add_node(&include.0);
|
||||||
|
self.graph.borrow_mut().add_edge(node, child, include.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn find_includes(&self, file: &Path) -> Vec<(PathBuf, IncludePosition)> {
|
||||||
|
let mut includes = Vec::default();
|
||||||
|
|
||||||
|
let buf = BufReader::new(std::fs::File::open(file).unwrap());
|
||||||
|
buf.lines()
|
||||||
|
.enumerate()
|
||||||
|
.filter_map(|line| match line.1 {
|
||||||
|
Ok(t) => Some((line.0, t)),
|
||||||
|
Err(_e) => None,
|
||||||
|
})
|
||||||
|
.filter(|line| RE_INCLUDE.is_match(line.1.as_str()))
|
||||||
|
.for_each(|line| {
|
||||||
|
let cap = RE_INCLUDE.captures(line.1.as_str()).unwrap().get(1).unwrap();
|
||||||
|
|
||||||
|
let start = cap.start();
|
||||||
|
let end = cap.end();
|
||||||
|
let mut path: String = cap.as_str().into();
|
||||||
|
|
||||||
|
let full_include = if path.starts_with('/') {
|
||||||
|
path = path.strip_prefix('/').unwrap().to_string();
|
||||||
|
self.root.join("shaders").join(PathBuf::from_slash(&path))
|
||||||
|
} else {
|
||||||
|
file.parent().unwrap().join(PathBuf::from_slash(&path))
|
||||||
|
};
|
||||||
|
|
||||||
|
includes.push((full_include, IncludePosition { line: line.0, start, end }));
|
||||||
|
});
|
||||||
|
|
||||||
|
includes
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update_includes(&self, file: &Path) {
|
||||||
|
let includes = self.find_includes(file);
|
||||||
|
|
||||||
|
info!("includes found for file"; "file" => file.to_str().unwrap(), "includes" => format!("{:?}", includes));
|
||||||
|
|
||||||
|
let idx = match self.graph.borrow_mut().find_node(file) {
|
||||||
|
None => return,
|
||||||
|
Some(n) => n,
|
||||||
|
};
|
||||||
|
|
||||||
|
let prev_children: HashSet<_> = HashSet::from_iter(self.graph.borrow().get_all_child_positions(idx).map(|tup| {
|
||||||
|
(self.graph.borrow().get_node(tup.0), tup.1)
|
||||||
|
}));
|
||||||
|
let new_children: HashSet<_> = includes.iter().cloned().collect();
|
||||||
|
|
||||||
|
let to_be_added = new_children.difference(&prev_children);
|
||||||
|
let to_be_removed = prev_children.difference(&new_children);
|
||||||
|
|
||||||
|
debug!(
|
||||||
|
"include sets diff'd";
|
||||||
|
"for removal" => format!("{:?}", to_be_removed),
|
||||||
|
"for addition" => format!("{:?}", to_be_added)
|
||||||
|
);
|
||||||
|
|
||||||
|
for removal in to_be_removed {
|
||||||
|
let child = self.graph.borrow_mut().find_node(&removal.0).unwrap();
|
||||||
|
self.graph.borrow_mut().remove_edge(idx, child, removal.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
for insertion in to_be_added {
|
||||||
|
self.add_include(includes.iter().find(|f| f.0 == *insertion.0).unwrap().clone(), idx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn lint(&self, uri: &Path) -> Result<HashMap<Url, Vec<Diagnostic>>> {
|
||||||
|
// get all top level ancestors of this file
|
||||||
|
let file_ancestors = match self.get_file_toplevel_ancestors(uri) {
|
||||||
|
Ok(opt) => match opt {
|
||||||
|
Some(ancestors) => ancestors,
|
||||||
|
None => vec![],
|
||||||
|
},
|
||||||
|
Err(e) => return Err(e),
|
||||||
|
};
|
||||||
|
|
||||||
|
info!(
|
||||||
|
"top-level file ancestors found";
|
||||||
|
"uri" => uri.to_str().unwrap(),
|
||||||
|
"ancestors" => format!("{:?}", file_ancestors
|
||||||
|
.iter()
|
||||||
|
.map(|e| PathBuf::from_str(
|
||||||
|
&self.graph.borrow().graph[*e].clone()
|
||||||
|
)
|
||||||
|
.unwrap())
|
||||||
|
.collect::<Vec<PathBuf>>())
|
||||||
|
);
|
||||||
|
|
||||||
|
// the set of all filepath->content.
|
||||||
|
let mut all_sources: HashMap<PathBuf, String> = HashMap::new();
|
||||||
|
// the set of filepath->list of diagnostics to report
|
||||||
|
let mut diagnostics: HashMap<Url, Vec<Diagnostic>> = HashMap::new();
|
||||||
|
|
||||||
|
// we want to backfill the diagnostics map with all linked sources
|
||||||
|
let back_fill = |all_sources: &HashMap<PathBuf, String>, diagnostics: &mut HashMap<Url, Vec<Diagnostic>>| {
|
||||||
|
for path in all_sources.keys() {
|
||||||
|
diagnostics.entry(Url::from_file_path(path).unwrap()).or_default();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// if we are a top-level file (this has to be one of the set defined by Optifine, right?)
|
||||||
|
if file_ancestors.is_empty() {
|
||||||
|
// gather the list of all descendants
|
||||||
|
let root = self.graph.borrow_mut().find_node(uri).unwrap();
|
||||||
|
let tree = match self.get_dfs_for_node(root) {
|
||||||
|
Ok(tree) => tree,
|
||||||
|
Err(e) => {
|
||||||
|
diagnostics.insert(Url::from_file_path(uri).unwrap(), vec![e.into()]);
|
||||||
|
return Ok(diagnostics);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
all_sources.extend(self.load_sources(&tree)?);
|
||||||
|
|
||||||
|
let mut source_mapper = source_mapper::SourceMapper::new(all_sources.len());
|
||||||
|
|
||||||
|
let view = {
|
||||||
|
let graph = self.graph.borrow();
|
||||||
|
let merged_string = {
|
||||||
|
merge_views::MergeViewBuilder::new(&tree, &all_sources, &graph, &mut source_mapper).build()
|
||||||
|
};
|
||||||
|
merged_string
|
||||||
|
};
|
||||||
|
|
||||||
|
let root_path = self.graph.borrow().get_node(root);
|
||||||
|
let ext = match root_path.extension() {
|
||||||
|
Some(ext) => ext.to_str().unwrap(),
|
||||||
|
None => {
|
||||||
|
back_fill(&all_sources, &mut diagnostics);
|
||||||
|
return Ok(diagnostics);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if !is_top_level(root_path.strip_prefix(&self.root).unwrap()) {
|
||||||
|
warn!("got a non-valid toplevel file"; "root_ancestor" => root_path.to_str().unwrap(), "stripped" => root_path.strip_prefix(&self.root).unwrap().to_str().unwrap());
|
||||||
|
back_fill(&all_sources, &mut diagnostics);
|
||||||
|
return Ok(diagnostics);
|
||||||
|
}
|
||||||
|
|
||||||
|
let tree_type = if ext == "fsh" {
|
||||||
|
TreeType::Fragment
|
||||||
|
} else if ext == "vsh" {
|
||||||
|
TreeType::Vertex
|
||||||
|
} else if ext == "gsh" {
|
||||||
|
TreeType::Geometry
|
||||||
|
} else if ext == "csh" {
|
||||||
|
TreeType::Compute
|
||||||
|
} else {
|
||||||
|
unreachable!();
|
||||||
|
};
|
||||||
|
|
||||||
|
let stdout = match self.compile_shader_source(&view, tree_type, &root_path) {
|
||||||
|
Some(s) => s,
|
||||||
|
None => {
|
||||||
|
back_fill(&all_sources, &mut diagnostics);
|
||||||
|
return Ok(diagnostics);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let diagnostics_parser = diagnostics_parser::DiagnosticsParser::new(self.opengl_context.as_ref());
|
||||||
|
|
||||||
|
diagnostics.extend(diagnostics_parser.parse_diagnostics_output(stdout, uri, &source_mapper, &self.graph.borrow()));
|
||||||
|
} else {
|
||||||
|
let mut all_trees: Vec<(TreeType, Vec<FilialTuple>)> = Vec::new();
|
||||||
|
|
||||||
|
for root in &file_ancestors {
|
||||||
|
let nodes = match self.get_dfs_for_node(*root) {
|
||||||
|
Ok(nodes) => nodes,
|
||||||
|
Err(e) => {
|
||||||
|
diagnostics.insert(Url::from_file_path(uri).unwrap(), vec![e.into()]);
|
||||||
|
back_fill(&all_sources, &mut diagnostics); // TODO: confirm
|
||||||
|
return Ok(diagnostics);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let root_path = self.graph.borrow().get_node(*root).clone();
|
||||||
|
let ext = match root_path.extension() {
|
||||||
|
Some(ext) => ext.to_str().unwrap(),
|
||||||
|
None => continue,
|
||||||
|
};
|
||||||
|
|
||||||
|
if !is_top_level(root_path.strip_prefix(&self.root).unwrap()) {
|
||||||
|
warn!("got a non-valid toplevel file"; "root_ancestor" => root_path.to_str().unwrap(), "stripped" => root_path.strip_prefix(&self.root).unwrap().to_str().unwrap());
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let tree_type = if ext == "fsh" {
|
||||||
|
TreeType::Fragment
|
||||||
|
} else if ext == "vsh" {
|
||||||
|
TreeType::Vertex
|
||||||
|
} else if ext == "gsh" {
|
||||||
|
TreeType::Geometry
|
||||||
|
} else if ext == "csh" {
|
||||||
|
TreeType::Compute
|
||||||
|
} else {
|
||||||
|
unreachable!();
|
||||||
|
};
|
||||||
|
|
||||||
|
let sources = self.load_sources(&nodes)?;
|
||||||
|
all_trees.push((tree_type, nodes));
|
||||||
|
all_sources.extend(sources);
|
||||||
|
}
|
||||||
|
|
||||||
|
for tree in all_trees {
|
||||||
|
// bit over-zealous in allocation but better than having to resize
|
||||||
|
let mut source_mapper = source_mapper::SourceMapper::new(all_sources.len());
|
||||||
|
let view = {
|
||||||
|
let graph = self.graph.borrow();
|
||||||
|
let merged_string = {
|
||||||
|
merge_views::MergeViewBuilder::new(&tree.1, &all_sources, &graph, &mut source_mapper).build()
|
||||||
|
};
|
||||||
|
merged_string
|
||||||
|
};
|
||||||
|
|
||||||
|
let root_path = self.graph.borrow().get_node(tree.1.first().unwrap().child);
|
||||||
|
let stdout = match self.compile_shader_source(&view, tree.0, &root_path) {
|
||||||
|
Some(s) => s,
|
||||||
|
None => continue,
|
||||||
|
};
|
||||||
|
|
||||||
|
let diagnostics_parser = diagnostics_parser::DiagnosticsParser::new(self.opengl_context.as_ref());
|
||||||
|
|
||||||
|
diagnostics.extend(diagnostics_parser.parse_diagnostics_output(stdout, uri, &source_mapper, &self.graph.borrow()));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
back_fill(&all_sources, &mut diagnostics);
|
||||||
|
Ok(diagnostics)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn compile_shader_source(&self, source: &str, tree_type: TreeType, path: &Path) -> Option<String> {
|
||||||
|
let result = self.opengl_context.clone().validate(tree_type, source);
|
||||||
|
match &result {
|
||||||
|
Some(output) => {
|
||||||
|
info!("compilation errors reported"; "errors" => format!("`{}`", output.replace('\n', "\\n")), "tree_root" => path.to_str().unwrap())
|
||||||
|
}
|
||||||
|
None => info!("compilation reported no errors"; "tree_root" => path.to_str().unwrap()),
|
||||||
|
};
|
||||||
|
result
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_dfs_for_node(&self, root: NodeIndex) -> Result<Vec<FilialTuple>, dfs::error::CycleError> {
|
||||||
|
let graph_ref = self.graph.borrow();
|
||||||
|
|
||||||
|
let dfs = dfs::Dfs::new(&graph_ref, root);
|
||||||
|
|
||||||
|
dfs.collect::<Result<_, _>>()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn load_sources(&self, nodes: &[FilialTuple]) -> Result<HashMap<PathBuf, String>> {
|
||||||
|
let mut sources = HashMap::new();
|
||||||
|
|
||||||
|
for node in nodes {
|
||||||
|
let graph = self.graph.borrow();
|
||||||
|
let path = graph.get_node(node.child);
|
||||||
|
|
||||||
|
if sources.contains_key(&path) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let source = match fs::read_to_string(&path) {
|
||||||
|
Ok(s) => s,
|
||||||
|
Err(e) => return Err(anyhow!("error reading {:?}: {}", path, e)),
|
||||||
|
};
|
||||||
|
let source = source.replace("\r\n", "\n");
|
||||||
|
sources.insert(path.clone(), source);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(sources)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_file_toplevel_ancestors(&self, uri: &Path) -> Result<Option<Vec<petgraph::stable_graph::NodeIndex>>> {
|
||||||
|
let curr_node = match self.graph.borrow_mut().find_node(uri) {
|
||||||
|
Some(n) => n,
|
||||||
|
None => return Err(anyhow!("node not found {:?}", uri)),
|
||||||
|
};
|
||||||
|
let roots = self.graph.borrow().collect_root_ancestors(curr_node);
|
||||||
|
if roots.is_empty() {
|
||||||
|
return Ok(None);
|
||||||
|
}
|
||||||
|
Ok(Some(roots))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn publish_diagnostic(&self, diagnostics: HashMap<Url, Vec<Diagnostic>>, document_version: Option<i32>) {
|
||||||
|
// info!("DIAGNOSTICS:\n{:?}", diagnostics);
|
||||||
|
for (uri, diagnostics) in diagnostics {
|
||||||
|
self.endpoint
|
||||||
|
.send_notification(
|
||||||
|
PublishDiagnostics::METHOD,
|
||||||
|
PublishDiagnosticsParams {
|
||||||
|
uri,
|
||||||
|
diagnostics,
|
||||||
|
version: document_version,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.expect("failed to publish diagnostics");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_status(&self, status: impl Into<String>, message: impl Into<String>, icon: impl Into<String>) {
|
||||||
|
self.endpoint
|
||||||
|
.send_notification(
|
||||||
|
lsp_ext::Status::METHOD,
|
||||||
|
lsp_ext::StatusParams {
|
||||||
|
status: status.into(),
|
||||||
|
message: Some(message.into()),
|
||||||
|
icon: Some(icon.into()),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.unwrap_or(());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl LanguageServerHandling for MinecraftShaderLanguageServer {
|
||||||
|
fn initialize(&mut self, params: InitializeParams, completable: MethodCompletable<InitializeResult, InitializeError>) {
|
||||||
|
logging::slog_with_trace_id(|| {
|
||||||
|
info!("starting server...");
|
||||||
|
|
||||||
|
let capabilities = ServerCapabilities {
|
||||||
|
definition_provider: Some(OneOf::Left(true)),
|
||||||
|
references_provider: Some(OneOf::Left(true)),
|
||||||
|
document_symbol_provider: Some(OneOf::Left(true)),
|
||||||
|
document_link_provider: Some(DocumentLinkOptions {
|
||||||
|
resolve_provider: None,
|
||||||
|
work_done_progress_options: WorkDoneProgressOptions { work_done_progress: None },
|
||||||
|
}),
|
||||||
|
execute_command_provider: Some(ExecuteCommandOptions {
|
||||||
|
commands: vec!["graphDot".into()],
|
||||||
|
work_done_progress_options: WorkDoneProgressOptions { work_done_progress: None },
|
||||||
|
}),
|
||||||
|
text_document_sync: Some(TextDocumentSyncCapability::Options(TextDocumentSyncOptions {
|
||||||
|
open_close: Some(true),
|
||||||
|
will_save: None,
|
||||||
|
will_save_wait_until: None,
|
||||||
|
change: Some(TextDocumentSyncKind::FULL),
|
||||||
|
save: Some(TextDocumentSyncSaveOptions::SaveOptions(SaveOptions { include_text: Some(true) })),
|
||||||
|
})),
|
||||||
|
..ServerCapabilities::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
let root = match params.root_uri {
|
||||||
|
Some(uri) => PathBuf::from_url(uri),
|
||||||
|
None => {
|
||||||
|
completable.complete(Err(MethodError {
|
||||||
|
code: 42069,
|
||||||
|
message: "Must be in workspace".into(),
|
||||||
|
data: InitializeError { retry: false },
|
||||||
|
}));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
completable.complete(Ok(InitializeResult {
|
||||||
|
capabilities,
|
||||||
|
server_info: None,
|
||||||
|
}));
|
||||||
|
|
||||||
|
self.set_status("loading", "Building dependency graph...", "$(loading~spin)");
|
||||||
|
|
||||||
|
self.root = root;
|
||||||
|
|
||||||
|
|
||||||
|
self.build_initial_graph();
|
||||||
|
|
||||||
|
self.set_status("ready", "Project initialized", "$(check)");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fn shutdown(&mut self, _: (), completable: LSCompletable<()>) {
|
||||||
|
warn!("shutting down language server...");
|
||||||
|
completable.complete(Ok(()));
|
||||||
|
}
|
||||||
|
|
||||||
|
fn exit(&mut self, _: ()) {
|
||||||
|
self.endpoint.request_shutdown();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn workspace_change_configuration(&mut self, params: DidChangeConfigurationParams) {
|
||||||
|
logging::slog_with_trace_id(|| {
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
struct Configuration {
|
||||||
|
#[serde(alias = "logLevel")]
|
||||||
|
log_level: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(settings) = params.settings.as_object().unwrap().get("mcglsl") {
|
||||||
|
let config: Configuration = from_value(settings.to_owned()).unwrap();
|
||||||
|
|
||||||
|
info!("got updated configuration"; "config" => params.settings.as_object().unwrap().get("mcglsl").unwrap().to_string());
|
||||||
|
|
||||||
|
configuration::handle_log_level_change(config.log_level, |level| {
|
||||||
|
self.log_guard = None; // set to None so Drop is invoked
|
||||||
|
self.log_guard = Some(logging::set_logger_with_level(level));
|
||||||
|
})
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fn did_open_text_document(&mut self, params: DidOpenTextDocumentParams) {
|
||||||
|
logging::slog_with_trace_id(|| {
|
||||||
|
//info!("opened doc {}", params.text_document.uri);
|
||||||
|
let path = PathBuf::from_url(params.text_document.uri);
|
||||||
|
if !path.starts_with(&self.root) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.graph.borrow_mut().find_node(&path) == None {
|
||||||
|
self.add_file_and_includes_to_graph(&path);
|
||||||
|
}
|
||||||
|
match self.lint(&path) {
|
||||||
|
Ok(diagnostics) => self.publish_diagnostic(diagnostics, None),
|
||||||
|
Err(e) => error!("error linting"; "error" => format!("{:?}", e), "path" => path.to_str().unwrap()),
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fn did_change_text_document(&mut self, _: DidChangeTextDocumentParams) {}
|
||||||
|
|
||||||
|
fn did_close_text_document(&mut self, _: DidCloseTextDocumentParams) {}
|
||||||
|
|
||||||
|
fn did_save_text_document(&mut self, params: DidSaveTextDocumentParams) {
|
||||||
|
logging::slog_with_trace_id(|| {
|
||||||
|
let path = PathBuf::from_url(params.text_document.uri);
|
||||||
|
if !path.starts_with(&self.root) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
self.update_includes(&path);
|
||||||
|
|
||||||
|
match self.lint(&path) {
|
||||||
|
Ok(diagnostics) => self.publish_diagnostic(diagnostics, None),
|
||||||
|
Err(e) => error!("error linting"; "error" => format!("{:?}", e), "path" => path.to_str().unwrap()),
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fn did_change_watched_files(&mut self, _: DidChangeWatchedFilesParams) {}
|
||||||
|
|
||||||
|
fn completion(&mut self, _: TextDocumentPositionParams, completable: LSCompletable<CompletionList>) {
|
||||||
|
completable.complete(Err(Self::error_not_available(())));
|
||||||
|
}
|
||||||
|
|
||||||
|
fn resolve_completion_item(&mut self, _: CompletionItem, completable: LSCompletable<CompletionItem>) {
|
||||||
|
completable.complete(Err(Self::error_not_available(())));
|
||||||
|
}
|
||||||
|
|
||||||
|
fn hover(&mut self, _: TextDocumentPositionParams, _: LSCompletable<Hover>) {
|
||||||
|
/* completable.complete(Ok(Hover{
|
||||||
|
contents: HoverContents::Markup(MarkupContent{
|
||||||
|
kind: MarkupKind::Markdown,
|
||||||
|
value: String::from("# Hello World"),
|
||||||
|
}),
|
||||||
|
range: None,
|
||||||
|
})); */
|
||||||
|
}
|
||||||
|
|
||||||
|
fn execute_command(&mut self, params: ExecuteCommandParams, completable: LSCompletable<Option<Value>>) {
|
||||||
|
logging::slog_with_trace_id(|| {
|
||||||
|
match self
|
||||||
|
.command_provider
|
||||||
|
.as_ref()
|
||||||
|
.unwrap()
|
||||||
|
.execute(¶ms.command, ¶ms.arguments, &self.root)
|
||||||
|
{
|
||||||
|
Ok(resp) => {
|
||||||
|
info!("executed command successfully"; "command" => params.command.clone());
|
||||||
|
self.endpoint
|
||||||
|
.send_notification(
|
||||||
|
ShowMessage::METHOD,
|
||||||
|
ShowMessageParams {
|
||||||
|
typ: MessageType::INFO,
|
||||||
|
message: format!("Command {} executed successfully.", params.command),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.expect("failed to send popup/show message notification");
|
||||||
|
completable.complete(Ok(Some(resp)))
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
error!("failed to execute command"; "command" => params.command.clone(), "error" => format!("{:?}", err));
|
||||||
|
self.endpoint
|
||||||
|
.send_notification(
|
||||||
|
ShowMessage::METHOD,
|
||||||
|
ShowMessageParams {
|
||||||
|
typ: MessageType::ERROR,
|
||||||
|
message: format!("Failed to execute `{}`. Reason: {}", params.command, err),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.expect("failed to send popup/show message notification");
|
||||||
|
completable.complete(Err(MethodError::new(32420, err.to_string(), ())))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fn signature_help(&mut self, _: TextDocumentPositionParams, completable: LSCompletable<SignatureHelp>) {
|
||||||
|
completable.complete(Err(Self::error_not_available(())));
|
||||||
|
}
|
||||||
|
|
||||||
|
fn goto_definition(&mut self, params: TextDocumentPositionParams, completable: LSCompletable<Vec<Location>>) {
|
||||||
|
logging::slog_with_trace_id(|| {
|
||||||
|
let path = PathBuf::from_url(params.text_document.uri);
|
||||||
|
if !path.starts_with(&self.root) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let parser = &mut self.tree_sitter.borrow_mut();
|
||||||
|
let parser_ctx = match navigation::ParserContext::new(parser, &path) {
|
||||||
|
Ok(ctx) => ctx,
|
||||||
|
Err(e) => {
|
||||||
|
return completable.complete(Err(MethodError {
|
||||||
|
code: 42069,
|
||||||
|
message: format!("error building parser context: error={}, path={:?}", e, path),
|
||||||
|
data: (),
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
match parser_ctx.find_definitions(&path, params.position) {
|
||||||
|
Ok(locations) => completable.complete(Ok(locations.unwrap_or_default())),
|
||||||
|
Err(e) => completable.complete(Err(MethodError {
|
||||||
|
code: 42069,
|
||||||
|
message: format!("error finding definitions: error={}, path={:?}", e, path),
|
||||||
|
data: (),
|
||||||
|
})),
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fn references(&mut self, params: ReferenceParams, completable: LSCompletable<Vec<Location>>) {
|
||||||
|
logging::slog_with_trace_id(|| {
|
||||||
|
let path = PathBuf::from_url(params.text_document_position.text_document.uri);
|
||||||
|
if !path.starts_with(&self.root) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let parser = &mut self.tree_sitter.borrow_mut();
|
||||||
|
let parser_ctx = match navigation::ParserContext::new(parser, &path) {
|
||||||
|
Ok(ctx) => ctx,
|
||||||
|
Err(e) => {
|
||||||
|
return completable.complete(Err(MethodError {
|
||||||
|
code: 42069,
|
||||||
|
message: format!("error building parser context: error={}, path={:?}", e, path),
|
||||||
|
data: (),
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
match parser_ctx.find_references(&path, params.text_document_position.position) {
|
||||||
|
Ok(locations) => completable.complete(Ok(locations.unwrap_or_default())),
|
||||||
|
Err(e) => completable.complete(Err(MethodError {
|
||||||
|
code: 42069,
|
||||||
|
message: format!("error finding definitions: error={}, path={:?}", e, path),
|
||||||
|
data: (),
|
||||||
|
})),
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fn document_highlight(&mut self, _: TextDocumentPositionParams, completable: LSCompletable<Vec<DocumentHighlight>>) {
|
||||||
|
completable.complete(Err(Self::error_not_available(())));
|
||||||
|
}
|
||||||
|
|
||||||
|
fn document_symbols(&mut self, params: DocumentSymbolParams, completable: LSCompletable<DocumentSymbolResponse>) {
|
||||||
|
logging::slog_with_trace_id(|| {
|
||||||
|
let path = PathBuf::from_url(params.text_document.uri);
|
||||||
|
if !path.starts_with(&self.root) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let parser = &mut self.tree_sitter.borrow_mut();
|
||||||
|
let parser_ctx = match navigation::ParserContext::new(parser, &path) {
|
||||||
|
Ok(ctx) => ctx,
|
||||||
|
Err(e) => {
|
||||||
|
return completable.complete(Err(MethodError {
|
||||||
|
code: 42069,
|
||||||
|
message: format!("error building parser context: error={}, path={:?}", e, path),
|
||||||
|
data: (),
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
match parser_ctx.list_symbols(&path) {
|
||||||
|
Ok(symbols) => completable.complete(Ok(DocumentSymbolResponse::from(symbols.unwrap_or_default()))),
|
||||||
|
Err(e) => {
|
||||||
|
return completable.complete(Err(MethodError {
|
||||||
|
code: 42069,
|
||||||
|
message: format!("error finding definitions: error={}, path={:?}", e, path),
|
||||||
|
data: (),
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fn workspace_symbols(&mut self, _: WorkspaceSymbolParams, completable: LSCompletable<DocumentSymbolResponse>) {
|
||||||
|
completable.complete(Err(Self::error_not_available(())));
|
||||||
|
}
|
||||||
|
|
||||||
|
fn code_action(&mut self, _: CodeActionParams, completable: LSCompletable<Vec<Command>>) {
|
||||||
|
completable.complete(Err(Self::error_not_available(())));
|
||||||
|
}
|
||||||
|
|
||||||
|
fn code_lens(&mut self, _: CodeLensParams, completable: LSCompletable<Vec<CodeLens>>) {
|
||||||
|
completable.complete(Err(Self::error_not_available(())));
|
||||||
|
}
|
||||||
|
|
||||||
|
fn code_lens_resolve(&mut self, _: CodeLens, completable: LSCompletable<CodeLens>) {
|
||||||
|
completable.complete(Err(Self::error_not_available(())));
|
||||||
|
}
|
||||||
|
|
||||||
|
fn document_link(&mut self, params: DocumentLinkParams, completable: LSCompletable<Vec<DocumentLink>>) {
|
||||||
|
logging::slog_with_trace_id(|| {
|
||||||
|
// node for current document
|
||||||
|
let curr_doc = PathBuf::from_url(params.text_document.uri);
|
||||||
|
let node = match self.graph.borrow_mut().find_node(&curr_doc) {
|
||||||
|
Some(n) => n,
|
||||||
|
None => {
|
||||||
|
warn!("document not found in graph"; "path" => curr_doc.to_str().unwrap());
|
||||||
|
completable.complete(Ok(vec![]));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let edges: Vec<DocumentLink> = self
|
||||||
|
.graph
|
||||||
|
.borrow()
|
||||||
|
.child_node_indexes(node)
|
||||||
|
.filter_map::<Vec<DocumentLink>, _>(|child| {
|
||||||
|
let graph = self.graph.borrow();
|
||||||
|
graph.get_child_positions(node, child).map(|value| {
|
||||||
|
let path = graph.get_node(child);
|
||||||
|
let url = match Url::from_file_path(&path) {
|
||||||
|
Ok(url) => url,
|
||||||
|
Err(e) => {
|
||||||
|
error!("error converting into url"; "path" => path.to_str().unwrap(), "error" => format!("{:?}", e));
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
Some(DocumentLink {
|
||||||
|
range: Range::new(
|
||||||
|
Position::new(u32::try_from(value.line).unwrap(), u32::try_from(value.start).unwrap()),
|
||||||
|
Position::new(u32::try_from(value.line).unwrap(), u32::try_from(value.end).unwrap()),
|
||||||
|
),
|
||||||
|
target: Some(url.clone()),
|
||||||
|
tooltip: Some(url.path().to_string()),
|
||||||
|
data: None,
|
||||||
|
})
|
||||||
|
}).collect()
|
||||||
|
})
|
||||||
|
.flatten()
|
||||||
|
.collect();
|
||||||
|
debug!("document link results";
|
||||||
|
"links" => format!("{:?}", edges.iter().map(|e| (e.range, e.target.as_ref().unwrap().path())).collect::<Vec<_>>()),
|
||||||
|
"path" => curr_doc.to_str().unwrap(),
|
||||||
|
);
|
||||||
|
completable.complete(Ok(edges));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fn document_link_resolve(&mut self, _: DocumentLink, completable: LSCompletable<DocumentLink>) {
|
||||||
|
completable.complete(Err(Self::error_not_available(())));
|
||||||
|
}
|
||||||
|
|
||||||
|
fn formatting(&mut self, _: DocumentFormattingParams, completable: LSCompletable<Vec<TextEdit>>) {
|
||||||
|
completable.complete(Err(Self::error_not_available(())));
|
||||||
|
}
|
||||||
|
|
||||||
|
fn range_formatting(&mut self, _: DocumentRangeFormattingParams, completable: LSCompletable<Vec<TextEdit>>) {
|
||||||
|
completable.complete(Err(Self::error_not_available(())));
|
||||||
|
}
|
||||||
|
|
||||||
|
fn on_type_formatting(&mut self, _: DocumentOnTypeFormattingParams, completable: LSCompletable<Vec<TextEdit>>) {
|
||||||
|
completable.complete(Err(Self::error_not_available(())));
|
||||||
|
}
|
||||||
|
|
||||||
|
fn rename(&mut self, _: RenameParams, completable: LSCompletable<WorkspaceEdit>) {
|
||||||
|
completable.complete(Err(Self::error_not_available(())));
|
||||||
|
}
|
||||||
|
}
|
645
server/main/src/merge_views.rs
Normal file
645
server/main/src/merge_views.rs
Normal file
|
@ -0,0 +1,645 @@
|
||||||
|
use std::cmp::min;
|
||||||
|
use std::iter::Peekable;
|
||||||
|
use std::{
|
||||||
|
collections::{HashMap, LinkedList, VecDeque},
|
||||||
|
path::{Path, PathBuf},
|
||||||
|
};
|
||||||
|
|
||||||
|
use core::slice::Iter;
|
||||||
|
|
||||||
|
use petgraph::stable_graph::NodeIndex;
|
||||||
|
use slog_scope::debug;
|
||||||
|
|
||||||
|
use crate::graph::CachedStableGraph;
|
||||||
|
use crate::source_mapper::SourceMapper;
|
||||||
|
use crate::IncludePosition;
|
||||||
|
|
||||||
|
/// FilialTuple represents a tuple (not really) of a child and any legitimate
|
||||||
|
/// parent. Parent can be nullable in the case of the child being a top level
|
||||||
|
/// node in the tree.
|
||||||
|
#[derive(Hash, PartialEq, Eq, Debug, Clone, Copy)]
|
||||||
|
pub struct FilialTuple {
|
||||||
|
pub child: NodeIndex,
|
||||||
|
pub parent: Option<NodeIndex>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Merges the source strings according to the nodes comprising a tree of imports into a GLSL source string
|
||||||
|
/// that can be handed off to the GLSL compiler.
|
||||||
|
pub struct MergeViewBuilder<'a> {
|
||||||
|
nodes: &'a [FilialTuple],
|
||||||
|
nodes_peeker: Peekable<Iter<'a, FilialTuple>>,
|
||||||
|
|
||||||
|
sources: &'a HashMap<PathBuf, String>,
|
||||||
|
graph: &'a CachedStableGraph,
|
||||||
|
source_mapper: &'a mut SourceMapper,
|
||||||
|
|
||||||
|
// holds the offset into the child which has been added to the merge list for a parent.
|
||||||
|
// A child can have multiple parents for a given tree, and be included multiple times
|
||||||
|
// by the same parent, hence we have to track it for a ((child, parent), line) tuple
|
||||||
|
// instead of just the child or (child, parent).
|
||||||
|
last_offset_set: HashMap<FilialTuple, usize>,
|
||||||
|
// holds, for any given filial tuple, the iterator yielding all the positions at which the child
|
||||||
|
// is included into the parent in line-sorted order. This is necessary for files that are imported
|
||||||
|
// more than once into the same parent, so we can easily get the next include position.
|
||||||
|
parent_child_edge_iterator: HashMap<FilialTuple, Box<(dyn Iterator<Item = IncludePosition> + 'a)>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> MergeViewBuilder<'a> {
|
||||||
|
pub fn new(
|
||||||
|
nodes: &'a [FilialTuple], sources: &'a HashMap<PathBuf, String>, graph: &'a CachedStableGraph, source_mapper: &'a mut SourceMapper,
|
||||||
|
) -> Self {
|
||||||
|
MergeViewBuilder {
|
||||||
|
nodes,
|
||||||
|
nodes_peeker: nodes.iter().peekable(),
|
||||||
|
sources,
|
||||||
|
graph,
|
||||||
|
source_mapper,
|
||||||
|
last_offset_set: HashMap::new(),
|
||||||
|
parent_child_edge_iterator: HashMap::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn build(&mut self) -> String {
|
||||||
|
// contains additionally inserted lines such as #line and other directives, preamble defines etc
|
||||||
|
let mut extra_lines: Vec<String> = Vec::new();
|
||||||
|
extra_lines.reserve((self.nodes.len() * 2) + 2);
|
||||||
|
|
||||||
|
// list of source code views onto the below sources
|
||||||
|
let mut merge_list: LinkedList<&'a str> = LinkedList::new();
|
||||||
|
|
||||||
|
// invariant: nodes_iter always has _at least_ one element. Can't save a not-file :B
|
||||||
|
let first = self.nodes_peeker.next().unwrap().child;
|
||||||
|
let first_path = self.graph.get_node(first);
|
||||||
|
let first_source = self.sources.get(&first_path).unwrap();
|
||||||
|
|
||||||
|
// seed source_mapper with top-level file
|
||||||
|
self.source_mapper.get_num(first);
|
||||||
|
|
||||||
|
let version_line_offset = self.find_version_offset(first_source);
|
||||||
|
let _version_char_offsets = self.char_offset_for_line(version_line_offset, first_source);
|
||||||
|
// add_preamble(
|
||||||
|
// version_line_offset,
|
||||||
|
// version_char_offsets.1,
|
||||||
|
// &first_path,
|
||||||
|
// first,
|
||||||
|
// first_source,
|
||||||
|
// &mut merge_list,
|
||||||
|
// &mut extra_lines,
|
||||||
|
// source_mapper,
|
||||||
|
// );
|
||||||
|
|
||||||
|
// last_offset_set.insert((first, None), version_char_offsets.1);
|
||||||
|
self.set_last_offset_for_tuple(None, first, 0);
|
||||||
|
|
||||||
|
// stack to keep track of the depth first traversal
|
||||||
|
let mut stack = VecDeque::<NodeIndex>::new();
|
||||||
|
|
||||||
|
self.create_merge_views(&mut merge_list, &mut extra_lines, &mut stack);
|
||||||
|
|
||||||
|
// now we add a view of the remainder of the root file
|
||||||
|
|
||||||
|
let offset = self.get_last_offset_for_tuple(None, first).unwrap();
|
||||||
|
|
||||||
|
let len = first_source.len();
|
||||||
|
merge_list.push_back(&first_source[min(offset, len)..]);
|
||||||
|
|
||||||
|
let total_len = merge_list.iter().fold(0, |a, b| a + b.len());
|
||||||
|
|
||||||
|
let mut merged = String::with_capacity(total_len);
|
||||||
|
merged.extend(merge_list);
|
||||||
|
|
||||||
|
merged
|
||||||
|
}
|
||||||
|
|
||||||
|
fn create_merge_views(&mut self, merge_list: &mut LinkedList<&'a str>, extra_lines: &mut Vec<String>, stack: &mut VecDeque<NodeIndex>) {
|
||||||
|
loop {
|
||||||
|
let n = match self.nodes_peeker.next() {
|
||||||
|
Some(n) => n,
|
||||||
|
None => return,
|
||||||
|
};
|
||||||
|
|
||||||
|
// invariant: never None as only the first element in `nodes` should have a None, which is popped off in the calling function
|
||||||
|
let (parent, child) = (n.parent.unwrap(), n.child);
|
||||||
|
// gets the next include position for the filial tuple, seeding if this is the first time querying this tuple
|
||||||
|
let edge = self
|
||||||
|
.parent_child_edge_iterator
|
||||||
|
.entry(*n)
|
||||||
|
.or_insert_with(|| {
|
||||||
|
let child_positions = self.graph.get_child_positions(parent, child);
|
||||||
|
Box::new(child_positions)
|
||||||
|
})
|
||||||
|
.next()
|
||||||
|
.unwrap();
|
||||||
|
let parent_path = self.graph.get_node(parent).clone();
|
||||||
|
let child_path = self.graph.get_node(child).clone();
|
||||||
|
|
||||||
|
let parent_source = self.sources.get(&parent_path).unwrap();
|
||||||
|
let (char_for_line, char_following_line) = self.char_offset_for_line(edge.line, parent_source);
|
||||||
|
|
||||||
|
let offset = *self
|
||||||
|
.set_last_offset_for_tuple(stack.back().copied(), parent, char_following_line)
|
||||||
|
.get_or_insert(0);
|
||||||
|
|
||||||
|
debug!("creating view to start child file";
|
||||||
|
"parent" => parent_path.to_str().unwrap(), "child" => child_path.to_str().unwrap(),
|
||||||
|
"grandparent" => stack.back().copied().map(|g| self.graph.get_node(g).to_str().unwrap().to_string()), // self.graph.get_node().to_str().unwrap(),
|
||||||
|
"last_parent_offset" => offset, "line" => edge.line, "char_for_line" => char_for_line,
|
||||||
|
"char_following_line" => char_following_line,
|
||||||
|
);
|
||||||
|
|
||||||
|
merge_list.push_back(&parent_source[offset..char_for_line]);
|
||||||
|
self.add_opening_line_directive(&child_path, child, merge_list, extra_lines);
|
||||||
|
|
||||||
|
match self.nodes_peeker.peek() {
|
||||||
|
Some(next) => {
|
||||||
|
let next = *next;
|
||||||
|
// if the next pair's parent is not a child of the current pair, we dump the rest of this childs source
|
||||||
|
if next.parent.unwrap() != child {
|
||||||
|
let child_source = self.sources.get(&child_path).unwrap();
|
||||||
|
// if ends in \n\n, we want to exclude the last \n for some reason. Ask optilad
|
||||||
|
let offset = {
|
||||||
|
match child_source.ends_with('\n') {
|
||||||
|
true => child_source.len() - 1,
|
||||||
|
false => child_source.len(),
|
||||||
|
}
|
||||||
|
};
|
||||||
|
merge_list.push_back(&child_source[..offset]);
|
||||||
|
self.set_last_offset_for_tuple(Some(parent), child, 0);
|
||||||
|
// +2 because edge.line is 0 indexed but #line is 1 indexed and references the *following* line
|
||||||
|
self.add_closing_line_directive(edge.line + 2, &parent_path, parent, merge_list, extra_lines);
|
||||||
|
// if the next pair's parent is not the current pair's parent, we need to bubble up
|
||||||
|
if stack.contains(&next.parent.unwrap()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
stack.push_back(parent);
|
||||||
|
self.create_merge_views(merge_list, extra_lines, stack);
|
||||||
|
stack.pop_back();
|
||||||
|
|
||||||
|
let offset = self.get_last_offset_for_tuple(Some(parent), child).unwrap();
|
||||||
|
let child_source = self.sources.get(&child_path).unwrap();
|
||||||
|
// this evaluates to false once the file contents have been exhausted aka offset = child_source.len() + 1
|
||||||
|
let end_offset = match child_source.ends_with('\n') {
|
||||||
|
true => 1,
|
||||||
|
false => 0,
|
||||||
|
};
|
||||||
|
if offset < child_source.len() - end_offset {
|
||||||
|
// if ends in \n\n, we want to exclude the last \n for some reason. Ask optilad
|
||||||
|
merge_list.push_back(&child_source[offset..child_source.len() - end_offset]);
|
||||||
|
self.set_last_offset_for_tuple(Some(parent), child, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// +2 because edge.line is 0 indexed but #line is 1 indexed and references the *following* line
|
||||||
|
self.add_closing_line_directive(edge.line + 2, &parent_path, parent, merge_list, extra_lines);
|
||||||
|
|
||||||
|
// we need to check the next item at the point of original return further down the callstack
|
||||||
|
if self.nodes_peeker.peek().is_some() && stack.contains(&self.nodes_peeker.peek().unwrap().parent.unwrap()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
let child_source = self.sources.get(&child_path).unwrap();
|
||||||
|
// if ends in \n\n, we want to exclude the last \n for some reason. Ask optilad
|
||||||
|
let offset = match child_source.ends_with('\n') {
|
||||||
|
true => child_source.len() - 1,
|
||||||
|
false => child_source.len(),
|
||||||
|
};
|
||||||
|
merge_list.push_back(&child_source[..offset]);
|
||||||
|
self.set_last_offset_for_tuple(Some(parent), child, 0);
|
||||||
|
// +2 because edge.line is 0 indexed but #line is 1 indexed and references the *following* line
|
||||||
|
self.add_closing_line_directive(edge.line + 2, &parent_path, parent, merge_list, extra_lines);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_last_offset_for_tuple(&mut self, parent: Option<NodeIndex>, child: NodeIndex, offset: usize) -> Option<usize> {
|
||||||
|
debug!("inserting last offset";
|
||||||
|
"parent" => parent.map(|p| self.graph.get_node(p).to_str().unwrap().to_string()),
|
||||||
|
"child" => self.graph.get_node(child).to_str().unwrap().to_string(),
|
||||||
|
"offset" => offset);
|
||||||
|
self.last_offset_set.insert(FilialTuple { child, parent }, offset)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_last_offset_for_tuple(&self, parent: Option<NodeIndex>, child: NodeIndex) -> Option<usize> {
|
||||||
|
self.last_offset_set.get(&FilialTuple { child, parent }).copied()
|
||||||
|
}
|
||||||
|
|
||||||
|
// returns the character offset + 1 of the end of line number `line` and the character
|
||||||
|
// offset + 1 for the end of the line after the previous one
|
||||||
|
fn char_offset_for_line(&self, line_num: usize, source: &str) -> (usize, usize) {
|
||||||
|
let mut char_for_line: usize = 0;
|
||||||
|
let mut char_following_line: usize = 0;
|
||||||
|
for (n, line) in source.lines().enumerate() {
|
||||||
|
if n == line_num {
|
||||||
|
char_following_line += line.len() + 1;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
char_for_line += line.len() + 1;
|
||||||
|
char_following_line = char_for_line;
|
||||||
|
}
|
||||||
|
(char_for_line, char_following_line)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn find_version_offset(&self, source: &str) -> usize {
|
||||||
|
source
|
||||||
|
.lines()
|
||||||
|
.enumerate()
|
||||||
|
.find(|(_, line)| line.starts_with("#version "))
|
||||||
|
.map_or(0, |(i, _)| i)
|
||||||
|
}
|
||||||
|
|
||||||
|
// fn add_preamble<'a>(
|
||||||
|
// version_line_offset: usize, version_char_offset: usize, path: &Path, node: NodeIndex, source: &'a str,
|
||||||
|
// merge_list: &mut LinkedList<&'a str>, extra_lines: &mut Vec<String>, source_mapper: &mut SourceMapper,
|
||||||
|
// ) {
|
||||||
|
// // TODO: Optifine #define preabmle
|
||||||
|
// merge_list.push_back(&source[..version_char_offset]);
|
||||||
|
// let google_line_directive = format!(
|
||||||
|
// "#extension GL_GOOGLE_cpp_style_line_directive : enable\n#line {} {} // {}\n",
|
||||||
|
// // +2 because 0 indexed but #line is 1 indexed and references the *following* line
|
||||||
|
// version_line_offset + 2,
|
||||||
|
// source_mapper.get_num(node),
|
||||||
|
// path.to_str().unwrap().replace('\\', "\\\\"),
|
||||||
|
// );
|
||||||
|
// extra_lines.push(google_line_directive);
|
||||||
|
// unsafe_get_and_insert(merge_list, extra_lines);
|
||||||
|
// }
|
||||||
|
|
||||||
|
fn add_opening_line_directive(
|
||||||
|
&mut self, path: &Path, node: NodeIndex, merge_list: &mut LinkedList<&str>, extra_lines: &mut Vec<String>,
|
||||||
|
) {
|
||||||
|
let line_directive = format!(
|
||||||
|
"#line 1 {} // {}\n",
|
||||||
|
self.source_mapper.get_num(node),
|
||||||
|
path.to_str().unwrap().replace('\\', "\\\\")
|
||||||
|
);
|
||||||
|
extra_lines.push(line_directive);
|
||||||
|
self.unsafe_get_and_insert(merge_list, extra_lines);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn add_closing_line_directive(
|
||||||
|
&mut self, line: usize, path: &Path, node: NodeIndex, merge_list: &mut LinkedList<&str>, extra_lines: &mut Vec<String>,
|
||||||
|
) {
|
||||||
|
// Optifine doesn't seem to add a leading newline if the previous line was a #line directive
|
||||||
|
let line_directive = if let Some(l) = merge_list.back() {
|
||||||
|
if l.trim().starts_with("#line") {
|
||||||
|
format!(
|
||||||
|
"#line {} {} // {}\n",
|
||||||
|
line,
|
||||||
|
self.source_mapper.get_num(node),
|
||||||
|
path.to_str().unwrap().replace('\\', "\\\\")
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
format!(
|
||||||
|
"\n#line {} {} // {}\n",
|
||||||
|
line,
|
||||||
|
self.source_mapper.get_num(node),
|
||||||
|
path.to_str().unwrap().replace('\\', "\\\\")
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
format!(
|
||||||
|
"\n#line {} {} // {}\n",
|
||||||
|
line,
|
||||||
|
self.source_mapper.get_num(node),
|
||||||
|
path.to_str().unwrap().replace('\\', "\\\\")
|
||||||
|
)
|
||||||
|
};
|
||||||
|
|
||||||
|
extra_lines.push(line_directive);
|
||||||
|
self.unsafe_get_and_insert(merge_list, extra_lines);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn unsafe_get_and_insert(&self, merge_list: &mut LinkedList<&str>, extra_lines: &[String]) {
|
||||||
|
// :^)
|
||||||
|
unsafe {
|
||||||
|
let vec_ptr_offset = extra_lines.as_ptr().add(extra_lines.len() - 1);
|
||||||
|
merge_list.push_back(&vec_ptr_offset.as_ref().unwrap()[..]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod merge_view_test {
|
||||||
|
use std::fs;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
use crate::merge_views::MergeViewBuilder;
|
||||||
|
use crate::source_mapper::SourceMapper;
|
||||||
|
use crate::test::{copy_to_and_set_root, new_temp_server};
|
||||||
|
use crate::IncludePosition;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[logging_macro::log_scope]
|
||||||
|
fn test_generate_merge_list_01() {
|
||||||
|
let mut server = new_temp_server(None);
|
||||||
|
|
||||||
|
let (_tmp_dir, tmp_path) = copy_to_and_set_root("./testdata/01", &mut server);
|
||||||
|
server.endpoint.request_shutdown();
|
||||||
|
|
||||||
|
let final_idx = server.graph.borrow_mut().add_node(&tmp_path.join("shaders").join("final.fsh"));
|
||||||
|
let common_idx = server.graph.borrow_mut().add_node(&tmp_path.join("shaders").join("common.glsl"));
|
||||||
|
|
||||||
|
server
|
||||||
|
.graph
|
||||||
|
.borrow_mut()
|
||||||
|
.add_edge(final_idx, common_idx, IncludePosition { line: 2, start: 0, end: 0 });
|
||||||
|
|
||||||
|
let nodes = server.get_dfs_for_node(final_idx).unwrap();
|
||||||
|
let sources = server.load_sources(&nodes).unwrap();
|
||||||
|
|
||||||
|
let graph_borrow = server.graph.borrow();
|
||||||
|
let mut source_mapper = SourceMapper::new(0);
|
||||||
|
let result = MergeViewBuilder::new(&nodes, &sources, &graph_borrow, &mut source_mapper).build();
|
||||||
|
|
||||||
|
let merge_file = tmp_path.join("shaders").join("final.fsh.merge");
|
||||||
|
|
||||||
|
let mut truth = fs::read_to_string(merge_file).unwrap();
|
||||||
|
// truth = truth.replacen(
|
||||||
|
// "!!",
|
||||||
|
// &tmp_path.join("shaders").join("final.fsh").to_str().unwrap().replace('\\', "\\\\"),
|
||||||
|
// 1,
|
||||||
|
// );
|
||||||
|
truth = truth.replacen(
|
||||||
|
"!!",
|
||||||
|
&tmp_path.join("shaders").join("common.glsl").to_str().unwrap().replace('\\', "\\\\"),
|
||||||
|
1,
|
||||||
|
);
|
||||||
|
truth = truth.replace(
|
||||||
|
"!!",
|
||||||
|
&tmp_path.join("shaders").join("final.fsh").to_str().unwrap().replace('\\', "\\\\"),
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(result, truth);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[logging_macro::log_scope]
|
||||||
|
fn test_generate_merge_list_02() {
|
||||||
|
let mut server = new_temp_server(None);
|
||||||
|
|
||||||
|
let (_tmp_dir, tmp_path) = copy_to_and_set_root("./testdata/02", &mut server);
|
||||||
|
server.endpoint.request_shutdown();
|
||||||
|
|
||||||
|
let final_idx = server.graph.borrow_mut().add_node(&tmp_path.join("shaders").join("final.fsh"));
|
||||||
|
let test_idx = server
|
||||||
|
.graph
|
||||||
|
.borrow_mut()
|
||||||
|
.add_node(&tmp_path.join("shaders").join("utils").join("test.glsl"));
|
||||||
|
let burger_idx = server
|
||||||
|
.graph
|
||||||
|
.borrow_mut()
|
||||||
|
.add_node(&tmp_path.join("shaders").join("utils").join("burger.glsl"));
|
||||||
|
let sample_idx = server
|
||||||
|
.graph
|
||||||
|
.borrow_mut()
|
||||||
|
.add_node(&tmp_path.join("shaders").join("utils").join("sample.glsl"));
|
||||||
|
|
||||||
|
server
|
||||||
|
.graph
|
||||||
|
.borrow_mut()
|
||||||
|
.add_edge(final_idx, sample_idx, IncludePosition { line: 2, start: 0, end: 0 });
|
||||||
|
server
|
||||||
|
.graph
|
||||||
|
.borrow_mut()
|
||||||
|
.add_edge(sample_idx, burger_idx, IncludePosition { line: 4, start: 0, end: 0 });
|
||||||
|
server
|
||||||
|
.graph
|
||||||
|
.borrow_mut()
|
||||||
|
.add_edge(sample_idx, test_idx, IncludePosition { line: 6, start: 0, end: 0 });
|
||||||
|
|
||||||
|
let nodes = server.get_dfs_for_node(final_idx).unwrap();
|
||||||
|
let sources = server.load_sources(&nodes).unwrap();
|
||||||
|
|
||||||
|
let graph_borrow = server.graph.borrow();
|
||||||
|
let mut source_mapper = SourceMapper::new(0);
|
||||||
|
let result = MergeViewBuilder::new(&nodes, &sources, &graph_borrow, &mut source_mapper).build();
|
||||||
|
|
||||||
|
let merge_file = tmp_path.join("shaders").join("final.fsh.merge");
|
||||||
|
|
||||||
|
let mut truth = fs::read_to_string(merge_file).unwrap();
|
||||||
|
|
||||||
|
// truth = truth.replacen(
|
||||||
|
// "!!",
|
||||||
|
// &tmp_path.join("shaders").join("final.fsh").to_str().unwrap().replace('\\', "\\\\"),
|
||||||
|
// 1,
|
||||||
|
// );
|
||||||
|
|
||||||
|
for file in &["sample.glsl", "burger.glsl", "sample.glsl", "test.glsl", "sample.glsl"] {
|
||||||
|
let path = tmp_path.clone();
|
||||||
|
truth = truth.replacen(
|
||||||
|
"!!",
|
||||||
|
&path
|
||||||
|
.join("shaders")
|
||||||
|
.join("utils")
|
||||||
|
.join(file)
|
||||||
|
.to_str()
|
||||||
|
.unwrap()
|
||||||
|
.replace('\\', "\\\\"),
|
||||||
|
1,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
truth = truth.replacen(
|
||||||
|
"!!",
|
||||||
|
&tmp_path.join("shaders").join("final.fsh").to_str().unwrap().replace('\\', "\\\\"),
|
||||||
|
1,
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(result, truth);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[logging_macro::log_scope]
|
||||||
|
fn test_generate_merge_list_03() {
|
||||||
|
let mut server = new_temp_server(None);
|
||||||
|
|
||||||
|
let (_tmp_dir, tmp_path) = copy_to_and_set_root("./testdata/03", &mut server);
|
||||||
|
server.endpoint.request_shutdown();
|
||||||
|
|
||||||
|
let final_idx = server.graph.borrow_mut().add_node(&tmp_path.join("shaders").join("final.fsh"));
|
||||||
|
let test_idx = server
|
||||||
|
.graph
|
||||||
|
.borrow_mut()
|
||||||
|
.add_node(&tmp_path.join("shaders").join("utils").join("test.glsl"));
|
||||||
|
let burger_idx = server
|
||||||
|
.graph
|
||||||
|
.borrow_mut()
|
||||||
|
.add_node(&tmp_path.join("shaders").join("utils").join("burger.glsl"));
|
||||||
|
let sample_idx = server
|
||||||
|
.graph
|
||||||
|
.borrow_mut()
|
||||||
|
.add_node(&tmp_path.join("shaders").join("utils").join("sample.glsl"));
|
||||||
|
|
||||||
|
server
|
||||||
|
.graph
|
||||||
|
.borrow_mut()
|
||||||
|
.add_edge(final_idx, sample_idx, IncludePosition { line: 2, start: 0, end: 0 });
|
||||||
|
server
|
||||||
|
.graph
|
||||||
|
.borrow_mut()
|
||||||
|
.add_edge(sample_idx, burger_idx, IncludePosition { line: 4, start: 0, end: 0 });
|
||||||
|
server
|
||||||
|
.graph
|
||||||
|
.borrow_mut()
|
||||||
|
.add_edge(sample_idx, test_idx, IncludePosition { line: 6, start: 0, end: 0 });
|
||||||
|
|
||||||
|
let nodes = server.get_dfs_for_node(final_idx).unwrap();
|
||||||
|
let sources = server.load_sources(&nodes).unwrap();
|
||||||
|
|
||||||
|
let graph_borrow = server.graph.borrow();
|
||||||
|
let mut source_mapper = SourceMapper::new(0);
|
||||||
|
let result = MergeViewBuilder::new(&nodes, &sources, &graph_borrow, &mut source_mapper).build();
|
||||||
|
|
||||||
|
let merge_file = tmp_path.join("shaders").join("final.fsh.merge");
|
||||||
|
|
||||||
|
let mut truth = fs::read_to_string(merge_file).unwrap();
|
||||||
|
|
||||||
|
// truth = truth.replacen(
|
||||||
|
// "!!",
|
||||||
|
// &tmp_path.join("shaders").join("final.fsh").to_str().unwrap().replace('\\', "\\\\"),
|
||||||
|
// 1,
|
||||||
|
// );
|
||||||
|
|
||||||
|
for file in &["sample.glsl", "burger.glsl", "sample.glsl", "test.glsl", "sample.glsl"] {
|
||||||
|
let path = tmp_path.clone();
|
||||||
|
truth = truth.replacen(
|
||||||
|
"!!",
|
||||||
|
&path
|
||||||
|
.join("shaders")
|
||||||
|
.join("utils")
|
||||||
|
.join(file)
|
||||||
|
.to_str()
|
||||||
|
.unwrap()
|
||||||
|
.replace('\\', "\\\\"),
|
||||||
|
1,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
truth = truth.replacen(
|
||||||
|
"!!",
|
||||||
|
&tmp_path.join("shaders").join("final.fsh").to_str().unwrap().replace('\\', "\\\\"),
|
||||||
|
1,
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(result, truth);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[logging_macro::log_scope]
|
||||||
|
fn test_generate_merge_list_04() {
|
||||||
|
let mut server = new_temp_server(None);
|
||||||
|
|
||||||
|
let (_tmp_dir, tmp_path) = copy_to_and_set_root("./testdata/04", &mut server);
|
||||||
|
server.endpoint.request_shutdown();
|
||||||
|
|
||||||
|
let final_idx = server.graph.borrow_mut().add_node(&tmp_path.join("shaders").join("final.fsh"));
|
||||||
|
let utilities_idx = server
|
||||||
|
.graph
|
||||||
|
.borrow_mut()
|
||||||
|
.add_node(&tmp_path.join("shaders").join("utils").join("utilities.glsl"));
|
||||||
|
let stuff1_idx = server
|
||||||
|
.graph
|
||||||
|
.borrow_mut()
|
||||||
|
.add_node(&tmp_path.join("shaders").join("utils").join("stuff1.glsl"));
|
||||||
|
let stuff2_idx = server
|
||||||
|
.graph
|
||||||
|
.borrow_mut()
|
||||||
|
.add_node(&tmp_path.join("shaders").join("utils").join("stuff2.glsl"));
|
||||||
|
let matrices_idx = server
|
||||||
|
.graph
|
||||||
|
.borrow_mut()
|
||||||
|
.add_node(&tmp_path.join("shaders").join("lib").join("matrices.glsl"));
|
||||||
|
|
||||||
|
server
|
||||||
|
.graph
|
||||||
|
.borrow_mut()
|
||||||
|
.add_edge(final_idx, utilities_idx, IncludePosition { line: 2, start: 0, end: 0 });
|
||||||
|
server
|
||||||
|
.graph
|
||||||
|
.borrow_mut()
|
||||||
|
.add_edge(utilities_idx, stuff1_idx, IncludePosition { line: 0, start: 0, end: 0 });
|
||||||
|
server
|
||||||
|
.graph
|
||||||
|
.borrow_mut()
|
||||||
|
.add_edge(utilities_idx, stuff2_idx, IncludePosition { line: 1, start: 0, end: 0 });
|
||||||
|
server
|
||||||
|
.graph
|
||||||
|
.borrow_mut()
|
||||||
|
.add_edge(final_idx, matrices_idx, IncludePosition { line: 3, start: 0, end: 0 });
|
||||||
|
|
||||||
|
let nodes = server.get_dfs_for_node(final_idx).unwrap();
|
||||||
|
let sources = server.load_sources(&nodes).unwrap();
|
||||||
|
|
||||||
|
let graph_borrow = server.graph.borrow();
|
||||||
|
let mut source_mapper = SourceMapper::new(0);
|
||||||
|
let result = MergeViewBuilder::new(&nodes, &sources, &graph_borrow, &mut source_mapper).build();
|
||||||
|
|
||||||
|
let merge_file = tmp_path.join("shaders").join("final.fsh.merge");
|
||||||
|
|
||||||
|
let mut truth = fs::read_to_string(merge_file).unwrap();
|
||||||
|
|
||||||
|
for file in &[
|
||||||
|
// PathBuf::new().join("final.fsh").to_str().unwrap(),
|
||||||
|
PathBuf::new().join("utils").join("utilities.glsl").to_str().unwrap(),
|
||||||
|
PathBuf::new().join("utils").join("stuff1.glsl").to_str().unwrap(),
|
||||||
|
PathBuf::new().join("utils").join("utilities.glsl").to_str().unwrap(),
|
||||||
|
PathBuf::new().join("utils").join("stuff2.glsl").to_str().unwrap(),
|
||||||
|
PathBuf::new().join("utils").join("utilities.glsl").to_str().unwrap(),
|
||||||
|
PathBuf::new().join("final.fsh").to_str().unwrap(),
|
||||||
|
PathBuf::new().join("lib").join("matrices.glsl").to_str().unwrap(),
|
||||||
|
PathBuf::new().join("final.fsh").to_str().unwrap(),
|
||||||
|
] {
|
||||||
|
let path = tmp_path.clone();
|
||||||
|
truth = truth.replacen("!!", &path.join("shaders").join(file).to_str().unwrap().replace('\\', "\\\\"), 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
assert_eq!(result, truth);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[logging_macro::log_scope]
|
||||||
|
fn test_generate_merge_list_06() {
|
||||||
|
let mut server = new_temp_server(None);
|
||||||
|
|
||||||
|
let (_tmp_dir, tmp_path) = copy_to_and_set_root("./testdata/06", &mut server);
|
||||||
|
server.endpoint.request_shutdown();
|
||||||
|
|
||||||
|
let final_idx = server.graph.borrow_mut().add_node(&tmp_path.join("shaders").join("final.fsh"));
|
||||||
|
let test_idx = server.graph.borrow_mut().add_node(&tmp_path.join("shaders").join("test.glsl"));
|
||||||
|
|
||||||
|
server
|
||||||
|
.graph
|
||||||
|
.borrow_mut()
|
||||||
|
.add_edge(final_idx, test_idx, IncludePosition { line: 3, start: 0, end: 0 });
|
||||||
|
server
|
||||||
|
.graph
|
||||||
|
.borrow_mut()
|
||||||
|
.add_edge(final_idx, test_idx, IncludePosition { line: 5, start: 0, end: 0 });
|
||||||
|
|
||||||
|
let nodes = server.get_dfs_for_node(final_idx).unwrap();
|
||||||
|
let sources = server.load_sources(&nodes).unwrap();
|
||||||
|
|
||||||
|
let graph_borrow = server.graph.borrow();
|
||||||
|
let mut source_mapper = SourceMapper::new(0);
|
||||||
|
let result = MergeViewBuilder::new(&nodes, &sources, &graph_borrow, &mut source_mapper).build();
|
||||||
|
|
||||||
|
let merge_file = tmp_path.join("shaders").join("final.fsh.merge");
|
||||||
|
|
||||||
|
let mut truth = fs::read_to_string(merge_file).unwrap();
|
||||||
|
|
||||||
|
for file in &[
|
||||||
|
// PathBuf::new().join("final.fsh").to_str().unwrap(),
|
||||||
|
PathBuf::new().join("test.glsl").to_str().unwrap(),
|
||||||
|
PathBuf::new().join("final.fsh").to_str().unwrap(),
|
||||||
|
PathBuf::new().join("test.glsl").to_str().unwrap(),
|
||||||
|
PathBuf::new().join("final.fsh").to_str().unwrap(),
|
||||||
|
] {
|
||||||
|
let path = tmp_path.clone();
|
||||||
|
truth = truth.replacen("!!", &path.join("shaders").join(file).to_str().unwrap().replace('\\', "\\\\"), 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
assert_eq!(result, truth);
|
||||||
|
}
|
||||||
|
}
|
429
server/main/src/navigation.rs
Normal file
429
server/main/src/navigation.rs
Normal file
|
@ -0,0 +1,429 @@
|
||||||
|
use std::{collections::HashMap, fs::read_to_string, path::Path, vec};
|
||||||
|
|
||||||
|
use anyhow::Result;
|
||||||
|
use rust_lsp::lsp_types::{DocumentSymbol, Location, Position, Range, SymbolKind};
|
||||||
|
use slog_scope::{debug, info, trace};
|
||||||
|
use tree_sitter::{Node, Parser, Point, Query, QueryCursor, Tree};
|
||||||
|
use url::Url;
|
||||||
|
|
||||||
|
use crate::linemap::LineMap;
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Hash, PartialEq, Eq, Default)]
|
||||||
|
struct SymbolName(String);
|
||||||
|
|
||||||
|
impl SymbolName {
|
||||||
|
// construct a new SymbolName from a node and its node ID for overload disambiguating.
|
||||||
|
fn new(node: &Node, source: &str, node_id: usize) -> Self {
|
||||||
|
let mut fqname = vec![format!("{}[{}]", node.utf8_text(source.as_bytes()).unwrap(), node_id)];
|
||||||
|
|
||||||
|
// first node will always have a parent
|
||||||
|
let mut prev = *node;
|
||||||
|
let mut node = node.parent().unwrap();
|
||||||
|
|
||||||
|
loop {
|
||||||
|
match (node.kind(), prev.kind()) {
|
||||||
|
("function_definition", "compound_statement") => {
|
||||||
|
let func_ident = node.child_by_field_name("declarator").unwrap().child(0).unwrap();
|
||||||
|
fqname.push(format!("{}[{}]", func_ident.utf8_text(source.as_bytes()).unwrap(), func_ident.id()));
|
||||||
|
}
|
||||||
|
("struct_specifier", "field_declaration_list") => {
|
||||||
|
let struct_ident = node.child_by_field_name("name").unwrap();
|
||||||
|
fqname.push(format!(
|
||||||
|
"{}[{}]",
|
||||||
|
struct_ident.utf8_text(source.as_bytes()).unwrap(),
|
||||||
|
struct_ident.id()
|
||||||
|
));
|
||||||
|
}
|
||||||
|
_ => (),
|
||||||
|
}
|
||||||
|
|
||||||
|
prev = node;
|
||||||
|
node = match node.parent() {
|
||||||
|
Some(n) => n,
|
||||||
|
None => break,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
fqname.reverse();
|
||||||
|
SymbolName(fqname.join("/"))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parent(&self) -> Option<Self> {
|
||||||
|
self.0.rsplit_once('/').map(|(left, _)| SymbolName(left.to_string()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl slog::Value for SymbolName {
|
||||||
|
fn serialize(&self, record: &slog::Record, key: slog::Key, serializer: &mut dyn slog::Serializer) -> slog::Result {
|
||||||
|
self.0.serialize(record, key, serializer)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
macro_rules! find_function_def_str {
|
||||||
|
() => {
|
||||||
|
r#"
|
||||||
|
(
|
||||||
|
(function_declarator
|
||||||
|
(identifier) @function)
|
||||||
|
(#match? @function "^{}$")
|
||||||
|
)
|
||||||
|
"#
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
macro_rules! find_function_refs_str {
|
||||||
|
() => {
|
||||||
|
r#"
|
||||||
|
(
|
||||||
|
(call_expression
|
||||||
|
(identifier) @call)
|
||||||
|
(#match? @call "^{}$")
|
||||||
|
)
|
||||||
|
"#
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
macro_rules! find_variable_def_str {
|
||||||
|
() => {
|
||||||
|
r#"
|
||||||
|
[
|
||||||
|
(init_declarator
|
||||||
|
(identifier) @variable)
|
||||||
|
|
||||||
|
(parameter_declaration
|
||||||
|
(identifier) @variable)
|
||||||
|
|
||||||
|
(declaration
|
||||||
|
(identifier) @variable)
|
||||||
|
|
||||||
|
(#match? @variable "^{}$")
|
||||||
|
]
|
||||||
|
"#
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const LIST_SYMBOLS_STR: &str = r#"
|
||||||
|
; global consts
|
||||||
|
(declaration
|
||||||
|
(type_qualifier) @const_qualifier
|
||||||
|
(init_declarator
|
||||||
|
(identifier) @const_ident))
|
||||||
|
(#match? @const_qualifier "^const")
|
||||||
|
|
||||||
|
; global uniforms, varyings, struct variables etc
|
||||||
|
(translation_unit
|
||||||
|
(declaration
|
||||||
|
(identifier) @ident))
|
||||||
|
|
||||||
|
; #defines
|
||||||
|
(preproc_def
|
||||||
|
(identifier) @define_ident)
|
||||||
|
|
||||||
|
; function definitions
|
||||||
|
(function_declarator
|
||||||
|
(identifier) @func_ident)
|
||||||
|
|
||||||
|
; struct definitions
|
||||||
|
(struct_specifier
|
||||||
|
(type_identifier) @struct_ident)
|
||||||
|
|
||||||
|
; struct fields
|
||||||
|
(struct_specifier
|
||||||
|
(field_declaration_list
|
||||||
|
(field_declaration
|
||||||
|
[
|
||||||
|
(field_identifier) @field_ident
|
||||||
|
(array_declarator
|
||||||
|
(field_identifier) @field_ident)
|
||||||
|
])) @field_list)
|
||||||
|
"#;
|
||||||
|
|
||||||
|
pub struct ParserContext<'a> {
|
||||||
|
source: String,
|
||||||
|
tree: Tree,
|
||||||
|
linemap: LineMap,
|
||||||
|
parser: &'a mut Parser,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> ParserContext<'a> {
|
||||||
|
pub fn new(parser: &'a mut Parser, path: &Path) -> Result<Self> {
|
||||||
|
let source = read_to_string(path)?;
|
||||||
|
|
||||||
|
let tree = parser.parse(&source, None).unwrap();
|
||||||
|
|
||||||
|
let linemap = LineMap::new(&source);
|
||||||
|
|
||||||
|
Ok(ParserContext {
|
||||||
|
source,
|
||||||
|
tree,
|
||||||
|
linemap,
|
||||||
|
parser,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn list_symbols(&self, _path: &Path) -> Result<Option<Vec<DocumentSymbol>>> {
|
||||||
|
let query = Query::new(tree_sitter_glsl::language(), LIST_SYMBOLS_STR)?;
|
||||||
|
let mut query_cursor = QueryCursor::new();
|
||||||
|
|
||||||
|
let mut parent_child_vec: Vec<(Option<SymbolName>, DocumentSymbol)> = vec![];
|
||||||
|
let mut fqname_to_index: HashMap<SymbolName, usize> = HashMap::new();
|
||||||
|
|
||||||
|
for (m, _) in query_cursor.captures(&query, self.root_node(), self.source.as_bytes()) {
|
||||||
|
if m.captures.is_empty() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut capture_iter = m.captures.iter();
|
||||||
|
|
||||||
|
let capture = capture_iter.next().unwrap();
|
||||||
|
let capture_name = query.capture_names()[capture.index as usize].as_str();
|
||||||
|
|
||||||
|
trace!("next capture name"; "name" => capture_name, "capture" => format!("{:?}", capture));
|
||||||
|
|
||||||
|
let (kind, node) = match capture_name {
|
||||||
|
"const_qualifier" => (SymbolKind::CONSTANT, capture_iter.next().unwrap().node),
|
||||||
|
"ident" => (SymbolKind::VARIABLE, capture.node),
|
||||||
|
"func_ident" => (SymbolKind::FUNCTION, capture.node),
|
||||||
|
"define_ident" => (SymbolKind::STRING, capture.node),
|
||||||
|
"struct_ident" => (SymbolKind::STRUCT, capture.node),
|
||||||
|
"field_list" => (SymbolKind::FIELD, capture_iter.next().unwrap().node),
|
||||||
|
_ => (SymbolKind::NULL, capture.node),
|
||||||
|
};
|
||||||
|
|
||||||
|
let range = Range {
|
||||||
|
start: Position {
|
||||||
|
line: node.start_position().row as u32,
|
||||||
|
character: node.start_position().column as u32,
|
||||||
|
},
|
||||||
|
end: Position {
|
||||||
|
line: node.end_position().row as u32,
|
||||||
|
character: node.end_position().column as u32,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
let name = node.utf8_text(self.source.as_bytes()).unwrap().to_string();
|
||||||
|
|
||||||
|
let fqname = SymbolName::new(&node, self.source.as_str(), node.id());
|
||||||
|
|
||||||
|
debug!("found symbol"; "node_name" => &name, "kind" => format!("{:?}", kind), "fqname" => &fqname);
|
||||||
|
|
||||||
|
let child_symbol = DocumentSymbol {
|
||||||
|
name,
|
||||||
|
detail: None,
|
||||||
|
kind,
|
||||||
|
tags: None,
|
||||||
|
deprecated: None,
|
||||||
|
range,
|
||||||
|
selection_range: range,
|
||||||
|
children: None,
|
||||||
|
};
|
||||||
|
parent_child_vec.push((fqname.parent(), child_symbol));
|
||||||
|
trace!("inserting fqname"; "fqname" => &fqname, "index" => parent_child_vec.len() - 1);
|
||||||
|
fqname_to_index.insert(fqname, parent_child_vec.len() - 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// let mut symbols = vec![];
|
||||||
|
for i in 1..parent_child_vec.len() {
|
||||||
|
let (left, right) = parent_child_vec.split_at_mut(i);
|
||||||
|
let parent = &right[0].0;
|
||||||
|
let child = &right[0].1;
|
||||||
|
if let Some(parent) = parent {
|
||||||
|
trace!("finding parent"; "parent_symbol_name" => &parent, "child" => format!("{:?}", child), "split_point" => i, "left_len" => left.len(), "right_len" => right.len());
|
||||||
|
let parent_index = fqname_to_index.get(parent).unwrap();
|
||||||
|
let parent_sym = &mut left[*parent_index];
|
||||||
|
parent_sym.1.children.get_or_insert_default().push(right[0].1.clone())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let symbols = parent_child_vec
|
||||||
|
.iter()
|
||||||
|
.filter(|tuple| tuple.0.is_none())
|
||||||
|
.map(|tuple| tuple.1.clone())
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
Ok(Some(symbols))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn find_definitions(&self, path: &Path, point: Position) -> Result<Option<Vec<Location>>> {
|
||||||
|
let current_node = match self.find_node_at_point(point) {
|
||||||
|
Some(node) => node,
|
||||||
|
None => return Ok(None),
|
||||||
|
};
|
||||||
|
|
||||||
|
let parent = match current_node.parent() {
|
||||||
|
Some(parent) => parent,
|
||||||
|
None => return Ok(None),
|
||||||
|
};
|
||||||
|
|
||||||
|
debug!("matching location lookup method for parent-child tuple"; "parent" => parent.kind(), "child" => current_node.kind());
|
||||||
|
|
||||||
|
let locations = match (current_node.kind(), parent.kind()) {
|
||||||
|
(_, "call_expression") => {
|
||||||
|
let query_str = format!(find_function_def_str!(), current_node.utf8_text(self.source.as_bytes())?);
|
||||||
|
self.simple_global_search(path, &query_str)?
|
||||||
|
}
|
||||||
|
("identifier", "argument_list")
|
||||||
|
| ("identifier", "field_expression")
|
||||||
|
| ("identifier", "binary_expression")
|
||||||
|
| ("identifier", "assignment_expression") => self.tree_climbing_search(path, current_node)?,
|
||||||
|
_ => return Ok(None),
|
||||||
|
};
|
||||||
|
|
||||||
|
info!("finished searching for definitions"; "count" => locations.len(), "definitions" => format!("{:?}", locations));
|
||||||
|
|
||||||
|
Ok(Some(locations))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn find_references(&self, path: &Path, point: Position) -> Result<Option<Vec<Location>>> {
|
||||||
|
let current_node = match self.find_node_at_point(point) {
|
||||||
|
Some(node) => node,
|
||||||
|
None => return Ok(None),
|
||||||
|
};
|
||||||
|
|
||||||
|
let parent = match current_node.parent() {
|
||||||
|
Some(parent) => parent,
|
||||||
|
None => return Ok(None),
|
||||||
|
};
|
||||||
|
|
||||||
|
let locations = match (current_node.kind(), parent.kind()) {
|
||||||
|
(_, "function_declarator") => {
|
||||||
|
let query_str = format!(find_function_refs_str!(), current_node.utf8_text(self.source.as_bytes())?);
|
||||||
|
self.simple_global_search(path, &query_str)?
|
||||||
|
}
|
||||||
|
_ => return Ok(None),
|
||||||
|
};
|
||||||
|
|
||||||
|
info!("finished searching for references"; "count" => locations.len(), "references" => format!("{:?}", locations));
|
||||||
|
|
||||||
|
Ok(Some(locations))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn tree_climbing_search(&self, path: &Path, start_node: Node) -> Result<Vec<Location>> {
|
||||||
|
let mut locations = vec![];
|
||||||
|
|
||||||
|
let node_text = start_node.utf8_text(self.source.as_bytes())?;
|
||||||
|
|
||||||
|
let query_str = format!(find_variable_def_str!(), node_text);
|
||||||
|
|
||||||
|
debug!("built query string"; "query" => &query_str);
|
||||||
|
|
||||||
|
let mut parent = start_node.parent();
|
||||||
|
|
||||||
|
loop {
|
||||||
|
if parent.is_none() {
|
||||||
|
trace!("no more parent left, found nothing");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
let query = Query::new(tree_sitter_glsl::language(), &query_str)?;
|
||||||
|
let mut query_cursor = QueryCursor::new();
|
||||||
|
|
||||||
|
trace!("running tree-sitter query for node"; "node" => format!("{:?}", parent.unwrap()), "node_text" => parent.unwrap().utf8_text(self.source.as_bytes()).unwrap());
|
||||||
|
|
||||||
|
for m in query_cursor.matches(&query, parent.unwrap(), self.source.as_bytes()) {
|
||||||
|
for capture in m.captures {
|
||||||
|
let start = capture.node.start_position();
|
||||||
|
let end = capture.node.end_position();
|
||||||
|
|
||||||
|
locations.push(Location {
|
||||||
|
uri: Url::from_file_path(path).unwrap(),
|
||||||
|
range: Range {
|
||||||
|
start: Position {
|
||||||
|
line: start.row as u32,
|
||||||
|
character: start.column as u32,
|
||||||
|
},
|
||||||
|
end: Position {
|
||||||
|
line: end.row as u32,
|
||||||
|
character: end.column as u32,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !locations.is_empty() {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
parent = parent.unwrap().parent();
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(locations)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn simple_global_search(&self, path: &Path, query_str: &str) -> Result<Vec<Location>> {
|
||||||
|
let query = Query::new(tree_sitter_glsl::language(), query_str)?;
|
||||||
|
let mut query_cursor = QueryCursor::new();
|
||||||
|
|
||||||
|
let mut locations = vec![];
|
||||||
|
|
||||||
|
for m in query_cursor.matches(&query, self.root_node(), self.source.as_bytes()) {
|
||||||
|
for capture in m.captures {
|
||||||
|
let start = capture.node.start_position();
|
||||||
|
let end = capture.node.end_position();
|
||||||
|
|
||||||
|
locations.push(Location {
|
||||||
|
uri: Url::from_file_path(path).unwrap(),
|
||||||
|
range: Range {
|
||||||
|
start: Position {
|
||||||
|
line: start.row as u32,
|
||||||
|
character: start.column as u32,
|
||||||
|
},
|
||||||
|
end: Position {
|
||||||
|
line: end.row as u32,
|
||||||
|
character: end.column as u32,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(locations)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn root_node(&self) -> Node {
|
||||||
|
self.tree.root_node()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn find_node_at_point(&self, pos: Position) -> Option<Node> {
|
||||||
|
// if we're at the end of an ident, we need to look _back_ one char instead
|
||||||
|
// for tree-sitter to find the right node.
|
||||||
|
let look_behind = {
|
||||||
|
let offset = self.linemap.offset_for_position(pos);
|
||||||
|
let char_at = self.source.as_bytes()[offset];
|
||||||
|
trace!("looking for non-alpha for point adjustment";
|
||||||
|
"offset" => offset,
|
||||||
|
"char" => char_at as char,
|
||||||
|
"point" => format!("{:?}", pos),
|
||||||
|
"look_behind" => !char_at.is_ascii_alphabetic());
|
||||||
|
!char_at.is_ascii_alphabetic()
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut start = Point {
|
||||||
|
row: pos.line as usize,
|
||||||
|
column: pos.character as usize,
|
||||||
|
};
|
||||||
|
let mut end = Point {
|
||||||
|
row: pos.line as usize,
|
||||||
|
column: pos.character as usize,
|
||||||
|
};
|
||||||
|
|
||||||
|
if look_behind {
|
||||||
|
start.column -= 1;
|
||||||
|
} else {
|
||||||
|
end.column += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
match self.root_node().named_descendant_for_point_range(start, end) {
|
||||||
|
Some(node) => {
|
||||||
|
debug!("found a node";
|
||||||
|
"node" => format!("{:?}", node),
|
||||||
|
"text" => node.utf8_text(self.source.as_bytes()).unwrap(),
|
||||||
|
"start" => format!("{}", start),
|
||||||
|
"end" => format!("{}", end));
|
||||||
|
Some(node)
|
||||||
|
}
|
||||||
|
None => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,21 +1,27 @@
|
||||||
|
use std::ffi::{CStr, CString};
|
||||||
use std::ptr;
|
use std::ptr;
|
||||||
use std::ffi::{CString, CStr};
|
|
||||||
|
use slog_scope::info;
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
use mockall::automock;
|
use mockall::automock;
|
||||||
|
|
||||||
#[cfg_attr(test, automock)]
|
#[cfg_attr(test, automock)]
|
||||||
pub trait ShaderValidator {
|
pub trait ShaderValidator {
|
||||||
fn validate(&self, tree_type: super::TreeType, source: String) -> Option<String>;
|
fn validate(&self, tree_type: super::TreeType, source: &str) -> Option<String>;
|
||||||
|
fn vendor(&self) -> String;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct OpenGLContext {
|
pub struct OpenGlContext {
|
||||||
_ctx: glutin::Context<glutin::PossiblyCurrent>
|
_ctx: glutin::Context<glutin::PossiblyCurrent>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl OpenGLContext {
|
impl OpenGlContext {
|
||||||
pub fn new() -> OpenGLContext {
|
pub fn new() -> OpenGlContext {
|
||||||
let events_loop = glutin::event_loop::EventLoop::new();
|
let events_loop = glutin::event_loop::EventLoop::new();
|
||||||
let gl_window = glutin::ContextBuilder::new().build_headless(&*events_loop, glutin::dpi::PhysicalSize::new(1, 1)).unwrap();
|
let gl_window = glutin::ContextBuilder::new()
|
||||||
|
.build_headless(&*events_loop, glutin::dpi::PhysicalSize::new(1, 1))
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
let gl_window = unsafe {
|
let gl_window = unsafe {
|
||||||
let gl_window = gl_window.make_current().unwrap();
|
let gl_window = gl_window.make_current().unwrap();
|
||||||
|
@ -23,20 +29,20 @@ impl OpenGLContext {
|
||||||
gl_window
|
gl_window
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let gl_ctx = OpenGlContext { _ctx: gl_window };
|
||||||
|
|
||||||
unsafe {
|
unsafe {
|
||||||
eprintln!(
|
info!(
|
||||||
"Using OpenGL device {} {} {}",
|
"OpenGL device";
|
||||||
String::from_utf8(CStr::from_ptr(gl::GetString(gl::VENDOR) as *const _).to_bytes().to_vec()).unwrap(),
|
"vendor" => gl_ctx.vendor(),
|
||||||
String::from_utf8(CStr::from_ptr(gl::GetString(gl::VERSION) as *const _).to_bytes().to_vec()).unwrap(),
|
"version" => String::from_utf8(CStr::from_ptr(gl::GetString(gl::VERSION) as *const _).to_bytes().to_vec()).unwrap(),
|
||||||
String::from_utf8(CStr::from_ptr(gl::GetString(gl::RENDERER) as *const _).to_bytes().to_vec()).unwrap()
|
"renderer" => String::from_utf8(CStr::from_ptr(gl::GetString(gl::RENDERER) as *const _).to_bytes().to_vec()).unwrap()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
OpenGLContext{
|
gl_ctx
|
||||||
_ctx: gl_window,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
unsafe fn compile_and_get_shader_log(&self, shader: gl::types::GLuint, source: String) -> Option<String> {
|
unsafe fn compile_and_get_shader_log(&self, shader: gl::types::GLuint, source: &str) -> Option<String> {
|
||||||
let mut success = i32::from(gl::FALSE);
|
let mut success = i32::from(gl::FALSE);
|
||||||
let c_str_frag = CString::new(source).unwrap();
|
let c_str_frag = CString::new(source).unwrap();
|
||||||
gl::ShaderSource(shader, 1, &c_str_frag.as_ptr(), ptr::null());
|
gl::ShaderSource(shader, 1, &c_str_frag.as_ptr(), ptr::null());
|
||||||
|
@ -48,7 +54,12 @@ impl OpenGLContext {
|
||||||
let mut info_len: gl::types::GLint = 0;
|
let mut info_len: gl::types::GLint = 0;
|
||||||
gl::GetShaderiv(shader, gl::INFO_LOG_LENGTH, &mut info_len);
|
gl::GetShaderiv(shader, gl::INFO_LOG_LENGTH, &mut info_len);
|
||||||
let mut info = vec![0u8; info_len as usize];
|
let mut info = vec![0u8; info_len as usize];
|
||||||
gl::GetShaderInfoLog(shader, info_len as gl::types::GLsizei, ptr::null_mut(), info.as_mut_ptr() as *mut gl::types::GLchar);
|
gl::GetShaderInfoLog(
|
||||||
|
shader,
|
||||||
|
info_len as gl::types::GLsizei,
|
||||||
|
ptr::null_mut(),
|
||||||
|
info.as_mut_ptr() as *mut gl::types::GLchar,
|
||||||
|
);
|
||||||
info.set_len((info_len - 1) as usize); // ignore null for str::from_utf8
|
info.set_len((info_len - 1) as usize); // ignore null for str::from_utf8
|
||||||
Some(String::from_utf8(info).unwrap())
|
Some(String::from_utf8(info).unwrap())
|
||||||
} else {
|
} else {
|
||||||
|
@ -59,8 +70,8 @@ impl OpenGLContext {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ShaderValidator for OpenGLContext {
|
impl ShaderValidator for OpenGlContext {
|
||||||
fn validate(&self, tree_type: super::TreeType, source: String) -> Option<String> {
|
fn validate(&self, tree_type: super::TreeType, source: &str) -> Option<String> {
|
||||||
unsafe {
|
unsafe {
|
||||||
match tree_type {
|
match tree_type {
|
||||||
crate::TreeType::Fragment => {
|
crate::TreeType::Fragment => {
|
||||||
|
@ -78,7 +89,16 @@ impl ShaderValidator for OpenGLContext {
|
||||||
let geometry_shader = gl::CreateShader(gl::GEOMETRY_SHADER);
|
let geometry_shader = gl::CreateShader(gl::GEOMETRY_SHADER);
|
||||||
self.compile_and_get_shader_log(geometry_shader, source)
|
self.compile_and_get_shader_log(geometry_shader, source)
|
||||||
}
|
}
|
||||||
|
crate::TreeType::Compute => {
|
||||||
|
// Compute shader
|
||||||
|
let compute_shader = gl::CreateShader(gl::COMPUTE_SHADER);
|
||||||
|
self.compile_and_get_shader_log(compute_shader, source)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn vendor(&self) -> String {
|
||||||
|
unsafe { String::from_utf8(CStr::from_ptr(gl::GetString(gl::VENDOR) as *const _).to_bytes().to_vec()).unwrap() }
|
||||||
|
}
|
||||||
}
|
}
|
52
server/main/src/source_mapper.rs
Normal file
52
server/main/src/source_mapper.rs
Normal file
|
@ -0,0 +1,52 @@
|
||||||
|
use std::{collections::HashMap, fmt::Display};
|
||||||
|
|
||||||
|
use petgraph::graph::NodeIndex;
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, PartialEq, Eq, Hash)]
|
||||||
|
pub struct SourceNum(usize);
|
||||||
|
|
||||||
|
impl Display for SourceNum {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
f.write_str(format!("{}", self.0).as_str())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<usize> for SourceNum {
|
||||||
|
fn from(val: usize) -> Self {
|
||||||
|
SourceNum(val)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Maps from a graph node index to a virtual OpenGL
|
||||||
|
// source number (for when building the merged source view),
|
||||||
|
// and in reverse (for when mapping from GLSL error source numbers to their source path).
|
||||||
|
// What is a source number: https://community.khronos.org/t/what-is-source-string-number/70976
|
||||||
|
pub struct SourceMapper {
|
||||||
|
next: SourceNum,
|
||||||
|
mapping: HashMap<NodeIndex, SourceNum>,
|
||||||
|
reverse_mapping: Vec<NodeIndex>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SourceMapper {
|
||||||
|
pub fn new(capacity: usize) -> Self {
|
||||||
|
SourceMapper {
|
||||||
|
next: SourceNum(0),
|
||||||
|
mapping: HashMap::with_capacity(capacity),
|
||||||
|
reverse_mapping: Vec::with_capacity(capacity),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_num(&mut self, node: NodeIndex) -> SourceNum {
|
||||||
|
let num = &*self.mapping.entry(node).or_insert_with(|| {
|
||||||
|
let next = self.next;
|
||||||
|
self.next.0 += 1;
|
||||||
|
self.reverse_mapping.push(node);
|
||||||
|
next
|
||||||
|
});
|
||||||
|
*num
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_node(&self, num: SourceNum) -> NodeIndex {
|
||||||
|
self.reverse_mapping[num.0]
|
||||||
|
}
|
||||||
|
}
|
281
server/main/src/test.rs
Normal file
281
server/main/src/test.rs
Normal file
|
@ -0,0 +1,281 @@
|
||||||
|
use super::*;
|
||||||
|
use std::fs;
|
||||||
|
use std::io;
|
||||||
|
use std::io::Result;
|
||||||
|
|
||||||
|
use pretty_assertions::assert_eq;
|
||||||
|
|
||||||
|
use tempdir::TempDir;
|
||||||
|
|
||||||
|
use fs_extra::{copy_items, dir};
|
||||||
|
|
||||||
|
use jsonrpc_common::*;
|
||||||
|
use jsonrpc_response::*;
|
||||||
|
|
||||||
|
struct StdoutNewline {
|
||||||
|
s: Box<dyn io::Write>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl io::Write for StdoutNewline {
|
||||||
|
fn write(&mut self, buf: &[u8]) -> Result<usize> {
|
||||||
|
let res = self.s.write(buf);
|
||||||
|
if buf[buf.len() - 1] == b"}"[0] {
|
||||||
|
#[allow(unused_variables)]
|
||||||
|
let res = self.s.write(b"\n\n");
|
||||||
|
}
|
||||||
|
res
|
||||||
|
}
|
||||||
|
|
||||||
|
fn flush(&mut self) -> Result<()> {
|
||||||
|
self.s.flush()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn new_temp_server(opengl_context: Option<Box<dyn opengl::ShaderValidator>>) -> MinecraftShaderLanguageServer {
|
||||||
|
let endpoint = LSPEndpoint::create_lsp_output_with_output_stream(|| StdoutNewline { s: Box::new(io::sink()) });
|
||||||
|
|
||||||
|
let context = opengl_context.unwrap_or_else(|| Box::new(opengl::MockShaderValidator::new()));
|
||||||
|
|
||||||
|
MinecraftShaderLanguageServer {
|
||||||
|
endpoint,
|
||||||
|
graph: Rc::new(RefCell::new(graph::CachedStableGraph::new())),
|
||||||
|
root: "".into(),
|
||||||
|
command_provider: None,
|
||||||
|
opengl_context: context.into(),
|
||||||
|
log_guard: None,
|
||||||
|
tree_sitter: Rc::new(RefCell::new(Parser::new())),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn copy_files(files: &str, dest: &TempDir) {
|
||||||
|
let opts = &dir::CopyOptions::new();
|
||||||
|
let files = fs::read_dir(files)
|
||||||
|
.unwrap()
|
||||||
|
.map(|e| String::from(e.unwrap().path().to_str().unwrap()))
|
||||||
|
.collect::<Vec<String>>();
|
||||||
|
copy_items(&files, dest.path().join("shaders"), opts).unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn copy_to_and_set_root(test_path: &str, server: &mut MinecraftShaderLanguageServer) -> (Rc<TempDir>, PathBuf) {
|
||||||
|
let (_tmp_dir, tmp_path) = copy_to_tmp_dir(test_path);
|
||||||
|
|
||||||
|
server.root = tmp_path.clone(); //format!("{}{}", "file://", tmp_path);
|
||||||
|
|
||||||
|
(_tmp_dir, tmp_path)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn copy_to_tmp_dir(test_path: &str) -> (Rc<TempDir>, PathBuf) {
|
||||||
|
let tmp_dir = Rc::new(TempDir::new("mcshader").unwrap());
|
||||||
|
fs::create_dir(tmp_dir.path().join("shaders")).unwrap();
|
||||||
|
|
||||||
|
copy_files(test_path, &tmp_dir);
|
||||||
|
|
||||||
|
let tmp_clone = tmp_dir.clone();
|
||||||
|
let tmp_path = tmp_clone.path().to_str().unwrap();
|
||||||
|
|
||||||
|
(tmp_dir, tmp_path.into())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(deprecated)]
|
||||||
|
#[test]
|
||||||
|
#[logging_macro::log_scope]
|
||||||
|
fn test_empty_initialize() {
|
||||||
|
let mut server = new_temp_server(None);
|
||||||
|
|
||||||
|
let tmp_dir = TempDir::new("mcshader").unwrap();
|
||||||
|
let tmp_path = tmp_dir.path();
|
||||||
|
|
||||||
|
let initialize_params = InitializeParams {
|
||||||
|
process_id: None,
|
||||||
|
root_path: None,
|
||||||
|
root_uri: Some(Url::from_directory_path(tmp_path).unwrap()),
|
||||||
|
client_info: None,
|
||||||
|
initialization_options: None,
|
||||||
|
capabilities: ClientCapabilities {
|
||||||
|
workspace: None,
|
||||||
|
text_document: None,
|
||||||
|
experimental: None,
|
||||||
|
window: None,
|
||||||
|
general: Option::None,
|
||||||
|
},
|
||||||
|
trace: None,
|
||||||
|
workspace_folders: None,
|
||||||
|
locale: Option::None,
|
||||||
|
};
|
||||||
|
|
||||||
|
let on_response = |resp: Option<Response>| {
|
||||||
|
assert!(resp.is_some());
|
||||||
|
let respu = resp.unwrap();
|
||||||
|
match respu.result_or_error {
|
||||||
|
ResponseResult::Result(_) => {}
|
||||||
|
ResponseResult::Error(e) => {
|
||||||
|
panic!("expected ResponseResult::Result(..), got {:?}", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let completable = MethodCompletable::new(ResponseCompletable::new(Some(Id::Number(1)), Box::new(on_response)));
|
||||||
|
server.initialize(initialize_params, completable);
|
||||||
|
|
||||||
|
assert_eq!(server.root, tmp_path);
|
||||||
|
|
||||||
|
assert_eq!(server.graph.borrow().graph.edge_count(), 0);
|
||||||
|
assert_eq!(server.graph.borrow().graph.node_count(), 0);
|
||||||
|
|
||||||
|
server.endpoint.request_shutdown();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(deprecated)]
|
||||||
|
#[test]
|
||||||
|
#[logging_macro::log_scope]
|
||||||
|
fn test_01_initialize() {
|
||||||
|
let mut server = new_temp_server(None);
|
||||||
|
|
||||||
|
let (_tmp_dir, tmp_path) = copy_to_tmp_dir("./testdata/01");
|
||||||
|
|
||||||
|
let initialize_params = InitializeParams {
|
||||||
|
process_id: None,
|
||||||
|
root_path: None,
|
||||||
|
root_uri: Some(Url::from_directory_path(tmp_path.clone()).unwrap()),
|
||||||
|
client_info: None,
|
||||||
|
initialization_options: None,
|
||||||
|
capabilities: ClientCapabilities {
|
||||||
|
workspace: None,
|
||||||
|
text_document: None,
|
||||||
|
experimental: None,
|
||||||
|
window: None,
|
||||||
|
general: Option::None,
|
||||||
|
},
|
||||||
|
trace: None,
|
||||||
|
workspace_folders: None,
|
||||||
|
locale: Option::None,
|
||||||
|
};
|
||||||
|
|
||||||
|
let on_response = |resp: Option<Response>| {
|
||||||
|
assert!(resp.is_some());
|
||||||
|
let respu = resp.unwrap();
|
||||||
|
match respu.result_or_error {
|
||||||
|
ResponseResult::Result(_) => {}
|
||||||
|
ResponseResult::Error(e) => {
|
||||||
|
panic!("expected ResponseResult::Result(..), got {:?}", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let completable = MethodCompletable::new(ResponseCompletable::new(Some(Id::Number(1)), Box::new(on_response)));
|
||||||
|
server.initialize(initialize_params, completable);
|
||||||
|
server.endpoint.request_shutdown();
|
||||||
|
|
||||||
|
// Assert there is one edge between two nodes
|
||||||
|
assert_eq!(server.graph.borrow().graph.edge_count(), 1);
|
||||||
|
|
||||||
|
let edge = server.graph.borrow().graph.edge_indices().next().unwrap();
|
||||||
|
let (node1, node2) = server.graph.borrow().graph.edge_endpoints(edge).unwrap();
|
||||||
|
|
||||||
|
// Assert the values of the two nodes in the tree
|
||||||
|
assert_eq!(
|
||||||
|
server.graph.borrow().graph[node1],
|
||||||
|
//format!("{:?}/{}/{}", tmp_path, "shaders", "final.fsh")
|
||||||
|
tmp_path.join("shaders").join("final.fsh").to_str().unwrap().to_string()
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
server.graph.borrow().graph[node2],
|
||||||
|
//format!("{:?}/{}/{}", tmp_path, "shaders", "common.glsl")
|
||||||
|
tmp_path.join("shaders").join("common.glsl").to_str().unwrap().to_string()
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(server.graph.borrow().graph.edge_weight(edge).unwrap().line, 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(deprecated)]
|
||||||
|
#[test]
|
||||||
|
#[logging_macro::log_scope]
|
||||||
|
fn test_05_initialize() {
|
||||||
|
let mut server = new_temp_server(None);
|
||||||
|
|
||||||
|
let (_tmp_dir, tmp_path) = copy_to_tmp_dir("./testdata/05");
|
||||||
|
|
||||||
|
let initialize_params = InitializeParams {
|
||||||
|
process_id: None,
|
||||||
|
root_path: None,
|
||||||
|
root_uri: Some(Url::from_directory_path(tmp_path.clone()).unwrap()),
|
||||||
|
client_info: None,
|
||||||
|
initialization_options: None,
|
||||||
|
capabilities: ClientCapabilities {
|
||||||
|
workspace: None,
|
||||||
|
text_document: None,
|
||||||
|
experimental: None,
|
||||||
|
window: None,
|
||||||
|
general: Option::None,
|
||||||
|
},
|
||||||
|
trace: None,
|
||||||
|
workspace_folders: None,
|
||||||
|
locale: Option::None,
|
||||||
|
};
|
||||||
|
|
||||||
|
let on_response = |resp: Option<Response>| {
|
||||||
|
assert!(resp.is_some());
|
||||||
|
let respu = resp.unwrap();
|
||||||
|
match respu.result_or_error {
|
||||||
|
ResponseResult::Result(_) => {}
|
||||||
|
ResponseResult::Error(e) => {
|
||||||
|
panic!("expected ResponseResult::Result(..), got {:?}", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let completable = MethodCompletable::new(ResponseCompletable::new(Some(Id::Number(1)), Box::new(on_response)));
|
||||||
|
server.initialize(initialize_params, completable);
|
||||||
|
server.endpoint.request_shutdown();
|
||||||
|
|
||||||
|
// Assert there is one edge between two nodes
|
||||||
|
assert_eq!(server.graph.borrow().graph.edge_count(), 3);
|
||||||
|
|
||||||
|
assert_eq!(server.graph.borrow().graph.node_count(), 4);
|
||||||
|
|
||||||
|
let pairs: HashSet<(PathBuf, PathBuf)> = vec![
|
||||||
|
(
|
||||||
|
tmp_path.join("shaders").join("final.fsh").to_str().unwrap().to_string().into(),
|
||||||
|
tmp_path.join("shaders").join("common.glsl").to_str().unwrap().to_string().into(),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
tmp_path.join("shaders").join("final.fsh").to_str().unwrap().to_string().into(),
|
||||||
|
tmp_path
|
||||||
|
.join("shaders")
|
||||||
|
.join("test")
|
||||||
|
.join("banana.glsl")
|
||||||
|
.to_str()
|
||||||
|
.unwrap()
|
||||||
|
.to_string()
|
||||||
|
.into(),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
tmp_path
|
||||||
|
.join("shaders")
|
||||||
|
.join("test")
|
||||||
|
.join("banana.glsl")
|
||||||
|
.to_str()
|
||||||
|
.unwrap()
|
||||||
|
.to_string()
|
||||||
|
.into(),
|
||||||
|
tmp_path
|
||||||
|
.join("shaders")
|
||||||
|
.join("test")
|
||||||
|
.join("burger.glsl")
|
||||||
|
.to_str()
|
||||||
|
.unwrap()
|
||||||
|
.to_string()
|
||||||
|
.into(),
|
||||||
|
),
|
||||||
|
]
|
||||||
|
.into_iter()
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
for edge in server.graph.borrow().graph.edge_indices() {
|
||||||
|
let endpoints = server.graph.borrow().graph.edge_endpoints(edge).unwrap();
|
||||||
|
let first = server.graph.borrow().get_node(endpoints.0);
|
||||||
|
let second = server.graph.borrow().get_node(endpoints.1);
|
||||||
|
let contains = pairs.contains(&(first.clone(), second.clone()));
|
||||||
|
assert!(contains, "doesn't contain ({:?}, {:?})", first, second);
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,55 +1,73 @@
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
use slog_scope::trace;
|
||||||
|
use anyhow::Result;
|
||||||
use path_slash::PathBufExt;
|
use path_slash::PathBufExt;
|
||||||
use url::Url;
|
use url::Url;
|
||||||
use anyhow::Result;
|
|
||||||
|
|
||||||
pub trait FromUrl {
|
pub trait FromUrl {
|
||||||
fn from_url(u: Url) -> Self;
|
fn from_url(u: Url) -> Self;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub trait FromJSON {
|
pub trait FromJson {
|
||||||
fn from_json(v: &serde_json::value::Value) -> Result<Self> where Self: Sized;
|
fn from_json(v: &serde_json::value::Value) -> Result<Self>
|
||||||
|
where
|
||||||
|
Self: Sized;
|
||||||
}
|
}
|
||||||
|
|
||||||
impl FromUrl for PathBuf {
|
impl FromUrl for PathBuf {
|
||||||
#[cfg(target_family = "windows")]
|
#[cfg(target_family = "windows")]
|
||||||
fn from_url(u: Url) -> Self {
|
fn from_url(u: Url) -> Self {
|
||||||
let path = percent_encoding::percent_decode_str(u.path().strip_prefix("/").unwrap()).decode_utf8().unwrap();
|
let path = percent_encoding::percent_decode_str(u.path().strip_prefix('/').unwrap())
|
||||||
|
.decode_utf8()
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
trace!("converted win path from url"; "old" => u.as_str(), "new" => path.to_string());
|
||||||
|
|
||||||
PathBuf::from_slash(path)
|
PathBuf::from_slash(path)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(target_family = "unix")]
|
#[cfg(target_family = "unix")]
|
||||||
fn from_url(u: Url) -> Self {
|
fn from_url(u: Url) -> Self {
|
||||||
let path = percent_encoding::percent_decode_str(u.path()).decode_utf8().unwrap();
|
let path = percent_encoding::percent_decode_str(u.path()).decode_utf8().unwrap();
|
||||||
|
|
||||||
|
trace!("converted unix path from url"; "old" => u.as_str(), "new" => path.to_string());
|
||||||
|
|
||||||
PathBuf::from_slash(path)
|
PathBuf::from_slash(path)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl FromJSON for PathBuf {
|
impl FromJson for PathBuf {
|
||||||
#[cfg(target_family = "windows")]
|
#[cfg(target_family = "windows")]
|
||||||
fn from_json(v: &serde_json::value::Value) -> Result<Self>
|
fn from_json(v: &serde_json::value::Value) -> Result<Self>
|
||||||
where Self: Sized {
|
where
|
||||||
|
Self: Sized,
|
||||||
|
{
|
||||||
if !v.is_string() {
|
if !v.is_string() {
|
||||||
return Err(anyhow::format_err!("cannot convert {:?} to PathBuf", v));
|
return Err(anyhow::format_err!("cannot convert {:?} to PathBuf", v));
|
||||||
}
|
}
|
||||||
let path = v.to_string();
|
let path = v.to_string();
|
||||||
let path = percent_encoding::percent_decode_str(
|
let path = percent_encoding::percent_decode_str(path.trim_start_matches('"').trim_end_matches('"').strip_prefix('/').unwrap())
|
||||||
path.trim_start_matches('"').trim_end_matches('"').strip_prefix("/").unwrap()
|
.decode_utf8()?;
|
||||||
).decode_utf8()?;
|
|
||||||
|
trace!("converted win path from json"; "old" => v.to_string(), "new" => path.to_string());
|
||||||
|
|
||||||
Ok(PathBuf::from_slash(path))
|
Ok(PathBuf::from_slash(path))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(target_family = "unix")]
|
#[cfg(target_family = "unix")]
|
||||||
fn from_json(v: &serde_json::value::Value) -> Result<Self>
|
fn from_json(v: &serde_json::value::Value) -> Result<Self>
|
||||||
where Self: Sized {
|
where
|
||||||
|
Self: Sized,
|
||||||
|
{
|
||||||
if !v.is_string() {
|
if !v.is_string() {
|
||||||
return Err(anyhow::format_err!("cannot convert {:?} to PathBuf", v));
|
return Err(anyhow::format_err!("cannot convert {:?} to PathBuf", v));
|
||||||
}
|
}
|
||||||
let path = v.to_string();
|
let path = v.to_string();
|
||||||
let path = percent_encoding::percent_decode_str(
|
let path = percent_encoding::percent_decode_str(path.trim_start_matches('"').trim_end_matches('"')).decode_utf8()?;
|
||||||
path.trim_start_matches('"').trim_end_matches('"')
|
|
||||||
).decode_utf8()?;
|
trace!("converted unix path from json"; "old" => v.to_string(), "new" => path.to_string());
|
||||||
|
|
||||||
Ok(PathBuf::from_slash(path))
|
Ok(PathBuf::from_slash(path))
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -1,10 +1,10 @@
|
||||||
#version 120
|
#version 120
|
||||||
|
|
||||||
#line 1 "!!"
|
#line 1 1 // !!
|
||||||
float test() {
|
float test() {
|
||||||
return 0.5;
|
return 0.5;
|
||||||
}
|
}
|
||||||
#line 4 "!!"
|
#line 4 0 // !!
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
gl_FragColor[0] = vec4(0.0);
|
gl_FragColor[0] = vec4(0.0);
|
|
@ -1,26 +1,26 @@
|
||||||
#version 120
|
#version 120
|
||||||
|
|
||||||
#line 1 "!!"
|
#line 1 1 // !!
|
||||||
int sample() {
|
int sample() {
|
||||||
return 5;
|
return 5;
|
||||||
}
|
}
|
||||||
|
|
||||||
#line 1 "!!"
|
#line 1 2 // !!
|
||||||
void burger() {
|
void burger() {
|
||||||
// sample text
|
// sample text
|
||||||
}
|
}
|
||||||
#line 6 "!!"
|
#line 6 1 // !!
|
||||||
|
|
||||||
#line 1 "!!"
|
#line 1 3 // !!
|
||||||
float test() {
|
float test() {
|
||||||
return 3.0;
|
return 3.0;
|
||||||
}
|
}
|
||||||
#line 8 "!!"
|
#line 8 1 // !!
|
||||||
|
|
||||||
int sample_more() {
|
int sample_more() {
|
||||||
return 5;
|
return 5;
|
||||||
}
|
}
|
||||||
#line 4 "!!"
|
#line 4 0 // !!
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
gl_FragColor = vec4(1.0, 1.0, 1.0, 1.0);
|
gl_FragColor = vec4(1.0, 1.0, 1.0, 1.0);
|
|
@ -1,22 +1,22 @@
|
||||||
#version 120
|
#version 120
|
||||||
|
|
||||||
#line 1 "!!"
|
#line 1 1 // !!
|
||||||
int sample() {
|
int sample() {
|
||||||
return 5;
|
return 5;
|
||||||
}
|
}
|
||||||
|
|
||||||
#line 1 "!!"
|
#line 1 2 // !!
|
||||||
void burger() {
|
void burger() {
|
||||||
// sample text
|
// sample text
|
||||||
}
|
}
|
||||||
#line 6 "!!"
|
#line 6 1 // !!
|
||||||
|
|
||||||
#line 1 "!!"
|
#line 1 3 // !!
|
||||||
float test() {
|
float test() {
|
||||||
return 3.0;
|
return 3.0;
|
||||||
}
|
}
|
||||||
#line 8 "!!"
|
#line 8 1 // !!
|
||||||
#line 4 "!!"
|
#line 4 0 // !!
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
gl_FragColor = vec4(1.0, 1.0, 1.0, 1.0);
|
gl_FragColor = vec4(1.0, 1.0, 1.0, 1.0);
|
23
server/main/testdata/04/final.fsh.merge
vendored
Normal file
23
server/main/testdata/04/final.fsh.merge
vendored
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
#version 120
|
||||||
|
|
||||||
|
#line 1 1 // !!
|
||||||
|
#line 1 2 // !!
|
||||||
|
void stuff1() {
|
||||||
|
|
||||||
|
}
|
||||||
|
#line 2 1 // !!
|
||||||
|
#line 1 3 // !!
|
||||||
|
void stuff2() {
|
||||||
|
|
||||||
|
}
|
||||||
|
#line 3 1 // !!
|
||||||
|
#line 4 0 // !!
|
||||||
|
#line 1 4 // !!
|
||||||
|
void matrix() {
|
||||||
|
|
||||||
|
}
|
||||||
|
#line 5 0 // !!
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
|
||||||
|
}
|
|
@ -1,5 +1,7 @@
|
||||||
#version 120
|
#version 120
|
||||||
|
|
||||||
|
#line 2 "!!"
|
||||||
|
|
||||||
#line 1 "!!"
|
#line 1 "!!"
|
||||||
float test() {
|
float test() {
|
||||||
return 0.5;
|
return 0.5;
|
9
server/main/testdata/06/final.fsh
vendored
Normal file
9
server/main/testdata/06/final.fsh
vendored
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
#version 120
|
||||||
|
|
||||||
|
#ifdef BANANA
|
||||||
|
#include "test.glsl"
|
||||||
|
#else
|
||||||
|
#include "test.glsl"
|
||||||
|
#endif
|
||||||
|
|
||||||
|
void main() {}
|
17
server/main/testdata/06/final.fsh.merge
vendored
Normal file
17
server/main/testdata/06/final.fsh.merge
vendored
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
#version 120
|
||||||
|
|
||||||
|
#ifdef BANANA
|
||||||
|
#line 1 1 // !!
|
||||||
|
int test() {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
#line 5 0 // !!
|
||||||
|
#else
|
||||||
|
#line 1 1 // !!
|
||||||
|
int test() {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
#line 7 0 // !!
|
||||||
|
#endif
|
||||||
|
|
||||||
|
void main() {}
|
3
server/main/testdata/06/test.glsl
vendored
Normal file
3
server/main/testdata/06/test.glsl
vendored
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
int test() {
|
||||||
|
return 1;
|
||||||
|
}
|
|
@ -1,161 +0,0 @@
|
||||||
use std::{collections::HashMap, path::PathBuf};
|
|
||||||
use std::rc::Rc;
|
|
||||||
use std::cell::RefCell;
|
|
||||||
use std::fs::OpenOptions;
|
|
||||||
use std::io::prelude::*;
|
|
||||||
|
|
||||||
use serde_json::Value;
|
|
||||||
|
|
||||||
use petgraph::{dot, graph::NodeIndex};
|
|
||||||
|
|
||||||
use anyhow::{Result, format_err};
|
|
||||||
|
|
||||||
use std::fs;
|
|
||||||
|
|
||||||
use crate::{graph::CachedStableGraph, merge_views, url_norm::FromJSON};
|
|
||||||
use crate::dfs;
|
|
||||||
|
|
||||||
pub struct CustomCommandProvider {
|
|
||||||
commands: HashMap<String, Box<dyn Invokeable>>
|
|
||||||
}
|
|
||||||
|
|
||||||
impl CustomCommandProvider {
|
|
||||||
pub fn new(commands: Vec<(&str, Box<dyn Invokeable>)>) -> CustomCommandProvider {
|
|
||||||
CustomCommandProvider{
|
|
||||||
commands: commands.into_iter().map(|tup| {
|
|
||||||
(tup.0.into(), tup.1)
|
|
||||||
}).collect(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn execute(&self, command: &str, args: Vec<Value>, root_path: &PathBuf) -> Result<Value> {
|
|
||||||
if self.commands.contains_key(command) {
|
|
||||||
return self.commands.get(command).unwrap().run_command(root_path, args);
|
|
||||||
}
|
|
||||||
Err(format_err!("command doesn't exist"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub trait Invokeable {
|
|
||||||
fn run_command(&self, root: &PathBuf, arguments: Vec<Value>) -> Result<Value>;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct GraphDotCommand {
|
|
||||||
pub graph: Rc<RefCell<CachedStableGraph>>
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Invokeable for GraphDotCommand {
|
|
||||||
fn run_command(&self, root: &PathBuf, _: Vec<Value>) -> Result<Value> {
|
|
||||||
let filepath = root.join("graph.dot");
|
|
||||||
eprintln!("generating dot file at {:?}", filepath);
|
|
||||||
let mut file = OpenOptions::new()
|
|
||||||
.truncate(true)
|
|
||||||
.write(true)
|
|
||||||
.create(true)
|
|
||||||
.open(filepath)
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
let mut write_data_closure = || -> Result<(), std::io::Error> {
|
|
||||||
let graph = self.graph.as_ref();
|
|
||||||
|
|
||||||
file.seek(std::io::SeekFrom::Start(0))?;
|
|
||||||
file.write_all(dot::Dot::new(&graph.borrow().graph).to_string().as_bytes())?;
|
|
||||||
file.flush()?;
|
|
||||||
file.seek(std::io::SeekFrom::Start(0))?;
|
|
||||||
Ok(())
|
|
||||||
};
|
|
||||||
|
|
||||||
match write_data_closure() {
|
|
||||||
Err(err) => Err(format_err!("Error generating graphviz data: {}", err)),
|
|
||||||
_ => Ok(Value::Null)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct VirtualMergedDocument {
|
|
||||||
pub graph: Rc<RefCell<CachedStableGraph>>
|
|
||||||
}
|
|
||||||
|
|
||||||
impl VirtualMergedDocument {
|
|
||||||
// TODO: DUPLICATE CODE
|
|
||||||
fn get_file_toplevel_ancestors(&self, uri: &PathBuf) -> Result<Option<Vec<petgraph::stable_graph::NodeIndex>>> {
|
|
||||||
let curr_node = match self.graph.borrow_mut().find_node(uri) {
|
|
||||||
Some(n) => n,
|
|
||||||
None => return Err(format_err!("node not found {:?}", uri)),
|
|
||||||
};
|
|
||||||
let roots = self.graph.borrow().collect_root_ancestors(curr_node);
|
|
||||||
if roots.is_empty() {
|
|
||||||
return Ok(None);
|
|
||||||
}
|
|
||||||
Ok(Some(roots))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_dfs_for_node(&self, root: NodeIndex) -> Result<Vec<(NodeIndex, Option<NodeIndex>)>, dfs::error::CycleError> {
|
|
||||||
let graph_ref = self.graph.borrow();
|
|
||||||
|
|
||||||
let dfs = dfs::Dfs::new(&graph_ref, root);
|
|
||||||
|
|
||||||
dfs.collect::<Result<Vec<_>, _>>()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn load_sources(&self, nodes: &[(NodeIndex, Option<NodeIndex>)]) -> Result<HashMap<PathBuf, String>> {
|
|
||||||
let mut sources = HashMap::new();
|
|
||||||
|
|
||||||
for node in nodes {
|
|
||||||
let graph = self.graph.borrow();
|
|
||||||
let path = graph.get_node(node.0);
|
|
||||||
|
|
||||||
if sources.contains_key(&path) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
let source = match fs::read_to_string(&path) {
|
|
||||||
Ok(s) => s,
|
|
||||||
Err(e) => return Err(format_err!("error reading {:?}: {}", path, e))
|
|
||||||
};
|
|
||||||
sources.insert(path.clone(), source);
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(sources)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Invokeable for VirtualMergedDocument {
|
|
||||||
fn run_command(&self, root: &PathBuf, arguments: Vec<Value>) -> Result<Value> {
|
|
||||||
let path = PathBuf::from_json(arguments.get(0).unwrap())?;
|
|
||||||
|
|
||||||
let file_ancestors = match self.get_file_toplevel_ancestors(&path) {
|
|
||||||
Ok(opt) => match opt {
|
|
||||||
Some(ancestors) => ancestors,
|
|
||||||
None => vec![],
|
|
||||||
},
|
|
||||||
Err(e) => return Err(e),
|
|
||||||
};
|
|
||||||
|
|
||||||
//eprintln!("ancestors for {}:\n\t{:?}", path, file_ancestors.iter().map(|e| self.graph.borrow().graph.node_weight(*e).unwrap().clone()).collect::<Vec<String>>());
|
|
||||||
|
|
||||||
// the set of all filepath->content. TODO: change to Url?
|
|
||||||
let mut all_sources: HashMap<PathBuf, String> = HashMap::new();
|
|
||||||
|
|
||||||
// if we are a top-level file (this has to be one of the set defined by Optifine, right?)
|
|
||||||
if file_ancestors.is_empty() {
|
|
||||||
// gather the list of all descendants
|
|
||||||
let root = self.graph.borrow_mut().find_node(&path).unwrap();
|
|
||||||
let tree = match self.get_dfs_for_node(root) {
|
|
||||||
Ok(tree) => tree,
|
|
||||||
Err(e) => return Err(e.into()),
|
|
||||||
};
|
|
||||||
|
|
||||||
let sources = match self.load_sources(&tree) {
|
|
||||||
Ok(s) => s,
|
|
||||||
Err(e) => return Err(e)
|
|
||||||
};
|
|
||||||
all_sources.extend(sources);
|
|
||||||
|
|
||||||
let graph = self.graph.borrow();
|
|
||||||
let view = merge_views::generate_merge_list(&tree, &all_sources, &graph);
|
|
||||||
return Ok(serde_json::value::Value::String(view));
|
|
||||||
}
|
|
||||||
return Err(format_err!("{:?} is not a top-level file aka has ancestors", path.strip_prefix(root).unwrap()))
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,156 +0,0 @@
|
||||||
use petgraph::stable_graph::NodeIndex;
|
|
||||||
|
|
||||||
use crate::graph::CachedStableGraph;
|
|
||||||
|
|
||||||
use anyhow::Result;
|
|
||||||
|
|
||||||
struct VisitCount {
|
|
||||||
node: NodeIndex,
|
|
||||||
touch: usize,
|
|
||||||
children: usize,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Performs a depth-first search with duplicates
|
|
||||||
pub struct Dfs<'a> {
|
|
||||||
stack: Vec<NodeIndex>,
|
|
||||||
graph: &'a CachedStableGraph,
|
|
||||||
cycle: Vec<VisitCount>
|
|
||||||
}
|
|
||||||
|
|
||||||
impl <'a> Dfs<'a> {
|
|
||||||
pub fn new(graph: &'a CachedStableGraph, start: NodeIndex) -> Self {
|
|
||||||
Dfs {
|
|
||||||
stack: vec![start],
|
|
||||||
graph,
|
|
||||||
cycle: Vec::new()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn reset_path_to_branch(&mut self) {
|
|
||||||
while let Some(par) = self.cycle.last_mut() {
|
|
||||||
par.touch += 1;
|
|
||||||
if par.touch > par.children {
|
|
||||||
self.cycle.pop();
|
|
||||||
} else {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn check_for_cycle(&self, children: &[NodeIndex]) -> Result<(), error::CycleError> {
|
|
||||||
for prev in &self.cycle {
|
|
||||||
for child in children {
|
|
||||||
if prev.node == *child {
|
|
||||||
let cycle_nodes: Vec<NodeIndex> = self.cycle.iter().map(|n| n.node).collect();
|
|
||||||
return Err(
|
|
||||||
error::CycleError::new(&cycle_nodes, *child, self.graph)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl <'a> Iterator for Dfs<'a> {
|
|
||||||
type Item = Result<(NodeIndex, Option<NodeIndex>), error::CycleError>;
|
|
||||||
|
|
||||||
fn next(&mut self) -> Option<Result<(NodeIndex, Option<NodeIndex>), error::CycleError>> {
|
|
||||||
let parent = match self.cycle.last() {
|
|
||||||
Some(p) => Some(p.node),
|
|
||||||
None => None,
|
|
||||||
};
|
|
||||||
|
|
||||||
if let Some(node) = self.stack.pop() {
|
|
||||||
self.cycle.push(VisitCount{
|
|
||||||
node,
|
|
||||||
children: self.graph.graph.edges(node).count(),
|
|
||||||
touch: 1,
|
|
||||||
});
|
|
||||||
|
|
||||||
let mut children = self.graph.child_node_indexes(node);
|
|
||||||
|
|
||||||
if !children.is_empty() {
|
|
||||||
// sort by line number in parent
|
|
||||||
children.sort_by(|x, y| {
|
|
||||||
let graph = &self.graph.graph;
|
|
||||||
let edge1 = graph.edge_weight(graph.find_edge(node, *x).unwrap()).unwrap();
|
|
||||||
let edge2 = graph.edge_weight(graph.find_edge(node, *y).unwrap()).unwrap();
|
|
||||||
|
|
||||||
edge2.line.cmp(&edge1.line)
|
|
||||||
});
|
|
||||||
|
|
||||||
match self.check_for_cycle(&children) {
|
|
||||||
Ok(_) => {}
|
|
||||||
Err(e) => return Some(Err(e)),
|
|
||||||
};
|
|
||||||
|
|
||||||
for child in children {
|
|
||||||
self.stack.push(child);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
self.reset_path_to_branch();
|
|
||||||
}
|
|
||||||
|
|
||||||
return Some(Ok((node, parent)));
|
|
||||||
}
|
|
||||||
None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub mod error {
|
|
||||||
use petgraph::stable_graph::NodeIndex;
|
|
||||||
|
|
||||||
use std::{fmt::{Debug, Display}, path::PathBuf, error::Error as StdError};
|
|
||||||
|
|
||||||
use crate::{graph::CachedStableGraph, consts};
|
|
||||||
|
|
||||||
use rust_lsp::lsp_types::{Diagnostic, DiagnosticSeverity, Position, Range};
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub struct CycleError(Vec<PathBuf>);
|
|
||||||
|
|
||||||
impl StdError for CycleError {}
|
|
||||||
|
|
||||||
impl CycleError {
|
|
||||||
pub fn new(nodes: &[NodeIndex], current_node: NodeIndex, graph: &CachedStableGraph) -> Self {
|
|
||||||
let mut resolved_nodes: Vec<PathBuf> = nodes.iter().map(|i| graph.get_node(*i).clone()).collect();
|
|
||||||
resolved_nodes.push(graph.get_node(current_node).clone());
|
|
||||||
CycleError(resolved_nodes)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Display for CycleError {
|
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
||||||
let mut disp = String::new();
|
|
||||||
disp.push_str(format!("Include cycle detected:\n{:?} imports ", self.0[0]).as_str());
|
|
||||||
for p in &self.0[1..self.0.len()-1] {
|
|
||||||
disp.push_str(format!("\n{:?}, which imports ", *p).as_str());
|
|
||||||
}
|
|
||||||
disp.push_str(format!("\n{:?}", self.0[self.0.len()-1]).as_str());
|
|
||||||
f.write_str(disp.as_str())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Into<Diagnostic> for CycleError {
|
|
||||||
fn into(self) -> Diagnostic {
|
|
||||||
Diagnostic{
|
|
||||||
severity: Some(DiagnosticSeverity::Error),
|
|
||||||
range: Range::new(Position::new(0, 0), Position::new(0, 500)),
|
|
||||||
source: Some(consts::SOURCE.into()),
|
|
||||||
message: self.into(),
|
|
||||||
code: None,
|
|
||||||
tags: None,
|
|
||||||
related_information: None,
|
|
||||||
code_description: Option::None,
|
|
||||||
data: Option::None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Into<String> for CycleError {
|
|
||||||
fn into(self) -> String {
|
|
||||||
format!("{}", self)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,150 +0,0 @@
|
||||||
use petgraph::stable_graph::StableDiGraph;
|
|
||||||
use petgraph::stable_graph::NodeIndex;
|
|
||||||
use petgraph::Direction;
|
|
||||||
use petgraph::stable_graph::EdgeIndex;
|
|
||||||
|
|
||||||
use std::{collections::{HashMap, HashSet}, path::PathBuf, str::FromStr};
|
|
||||||
|
|
||||||
use super::IncludePosition;
|
|
||||||
|
|
||||||
/// Wraps a `StableDiGraph` with caching behaviour for node search by maintaining
|
|
||||||
/// an index for node value to node index and a reverse index.
|
|
||||||
/// This allows for **O(1)** lookup for a value if it exists, else **O(n)**.
|
|
||||||
pub struct CachedStableGraph {
|
|
||||||
// StableDiGraph is used as it allows for String node values, essential for
|
|
||||||
// generating the GraphViz DOT render.
|
|
||||||
pub graph: StableDiGraph<String, IncludePosition>,
|
|
||||||
cache: HashMap<PathBuf, NodeIndex>,
|
|
||||||
// Maps a node index to its abstracted string representation.
|
|
||||||
// Mainly used as the graph is based on NodeIndex and
|
|
||||||
reverse_index: HashMap<NodeIndex, PathBuf>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl CachedStableGraph {
|
|
||||||
pub fn new() -> CachedStableGraph {
|
|
||||||
CachedStableGraph{
|
|
||||||
graph: StableDiGraph::new(),
|
|
||||||
cache: HashMap::new(),
|
|
||||||
reverse_index: HashMap::new(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns the `NodeIndex` for a given graph node with the value of `name`
|
|
||||||
/// and caches the result in the `HashMap`. Complexity is **O(1)** if the value
|
|
||||||
/// is cached (which should always be the case), else **O(n)** where **n** is
|
|
||||||
/// the number of node indices, as an exhaustive search must be done.
|
|
||||||
pub fn find_node(&mut self, name: &PathBuf) -> Option<NodeIndex> {
|
|
||||||
match self.cache.get(name) {
|
|
||||||
Some(n) => Some(*n),
|
|
||||||
None => {
|
|
||||||
// If the string is not in cache, O(n) search the graph (i know...) and then cache the NodeIndex
|
|
||||||
// for later
|
|
||||||
let n = self.graph.node_indices().find(|n| self.graph[*n] == name.to_str().unwrap().to_string());
|
|
||||||
if let Some(n) = n {
|
|
||||||
self.cache.insert(name.into(), n);
|
|
||||||
}
|
|
||||||
n
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_node(&self, node: NodeIndex) -> PathBuf {
|
|
||||||
PathBuf::from_str(&self.graph[node]).unwrap()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_edge_meta(&self, parent: NodeIndex, child: NodeIndex) -> &IncludePosition {
|
|
||||||
self.graph.edge_weight(self.graph.find_edge(parent, child).unwrap()).unwrap()
|
|
||||||
}
|
|
||||||
|
|
||||||
#[allow(dead_code)]
|
|
||||||
pub fn remove_node(&mut self, name: &PathBuf) {
|
|
||||||
let idx = self.cache.remove(name);
|
|
||||||
if let Some(idx) = idx {
|
|
||||||
self.graph.remove_node(idx);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn add_node(&mut self, name: &PathBuf) -> NodeIndex {
|
|
||||||
if let Some(idx) = self.cache.get(name) {
|
|
||||||
return *idx;
|
|
||||||
}
|
|
||||||
let idx = self.graph.add_node(name.to_str().unwrap().to_string());
|
|
||||||
self.cache.insert(name.clone(), idx);
|
|
||||||
self.reverse_index.insert(idx, name.clone());
|
|
||||||
idx
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn add_edge(&mut self, parent: NodeIndex, child: NodeIndex, meta: IncludePosition) -> EdgeIndex {
|
|
||||||
self.graph.add_edge(parent, child, meta)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn remove_edge(&mut self, parent: NodeIndex, child: NodeIndex) {
|
|
||||||
let edge = self.graph.find_edge(parent, child).unwrap();
|
|
||||||
self.graph.remove_edge(edge);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[allow(dead_code)]
|
|
||||||
pub fn edge_weights(&self, node: NodeIndex) -> Vec<IncludePosition> {
|
|
||||||
self.graph.edges(node).map(|e| e.weight().clone()).collect()
|
|
||||||
}
|
|
||||||
|
|
||||||
#[allow(dead_code)]
|
|
||||||
pub fn child_node_names(&self, node: NodeIndex) -> Vec<PathBuf> {
|
|
||||||
self.graph.neighbors(node).map(|n| self.reverse_index.get(&n).unwrap().clone()).collect()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn child_node_meta(&self, node: NodeIndex) -> Vec<(PathBuf, IncludePosition)> {
|
|
||||||
self.graph.neighbors(node).map(|n| {
|
|
||||||
let edge = self.graph.find_edge(node, n).unwrap();
|
|
||||||
let edge_meta = self.graph.edge_weight(edge).unwrap();
|
|
||||||
return (self.reverse_index.get(&n).unwrap().clone(), edge_meta.clone())
|
|
||||||
}).collect()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn child_node_indexes(&self, node: NodeIndex) -> Vec<NodeIndex> {
|
|
||||||
self.graph.neighbors(node).collect()
|
|
||||||
}
|
|
||||||
|
|
||||||
#[allow(dead_code)]
|
|
||||||
pub fn parent_node_names(&self, node: NodeIndex) -> Vec<PathBuf> {
|
|
||||||
self.graph.neighbors_directed(node, Direction::Incoming).map(|n| self.reverse_index.get(&n).unwrap().clone()).collect()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn parent_node_indexes(&self, node: NodeIndex) -> Vec<NodeIndex> {
|
|
||||||
self.graph.neighbors_directed(node, Direction::Incoming).collect()
|
|
||||||
}
|
|
||||||
|
|
||||||
#[allow(dead_code)]
|
|
||||||
pub fn get_include_meta(&self, node: NodeIndex) -> Vec<IncludePosition> {
|
|
||||||
self.graph.edges(node).map(|e| e.weight().clone()).collect()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn collect_root_ancestors(&self, node: NodeIndex) -> Vec<NodeIndex> {
|
|
||||||
let mut visited = HashSet::new();
|
|
||||||
self.get_root_ancestors(node, node, &mut visited)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_root_ancestors(&self, initial: NodeIndex, node: NodeIndex, visited: &mut HashSet<NodeIndex>) -> Vec<NodeIndex> {
|
|
||||||
if node == initial && !visited.is_empty() {
|
|
||||||
return vec![];
|
|
||||||
}
|
|
||||||
|
|
||||||
let parents = self.parent_node_indexes(node);
|
|
||||||
let mut collection = Vec::with_capacity(parents.len());
|
|
||||||
|
|
||||||
for ancestor in &parents {
|
|
||||||
visited.insert(*ancestor);
|
|
||||||
}
|
|
||||||
|
|
||||||
for ancestor in &parents {
|
|
||||||
let ancestors = self.parent_node_indexes(*ancestor);
|
|
||||||
if !ancestors.is_empty() {
|
|
||||||
collection.extend(self.get_root_ancestors(initial, *ancestor, visited));
|
|
||||||
} else {
|
|
||||||
collection.push(*ancestor);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
collection
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,749 +0,0 @@
|
||||||
use rust_lsp::jsonrpc::{*, method_types::*};
|
|
||||||
use rust_lsp::lsp::*;
|
|
||||||
use rust_lsp::lsp_types::{*, notification::*};
|
|
||||||
|
|
||||||
use petgraph::stable_graph::NodeIndex;
|
|
||||||
|
|
||||||
use serde_json::Value;
|
|
||||||
use url_norm::FromUrl;
|
|
||||||
use walkdir::WalkDir;
|
|
||||||
|
|
||||||
use std::{cell::RefCell, path::PathBuf, str::FromStr};
|
|
||||||
use std::collections::{HashMap, HashSet};
|
|
||||||
use std::collections::hash_map::RandomState;
|
|
||||||
use std::convert::TryFrom;
|
|
||||||
use std::fmt::{Display, Formatter, Debug};
|
|
||||||
use std::io::{stdin, stdout, BufRead, BufReader};
|
|
||||||
use std::rc::Rc;
|
|
||||||
use std::fs;
|
|
||||||
use std::iter::{Extend, FromIterator};
|
|
||||||
|
|
||||||
use path_slash::PathBufExt;
|
|
||||||
|
|
||||||
use anyhow::{Result, anyhow};
|
|
||||||
|
|
||||||
use chan::WaitGroup;
|
|
||||||
|
|
||||||
use regex::Regex;
|
|
||||||
|
|
||||||
use lazy_static::lazy_static;
|
|
||||||
|
|
||||||
mod graph;
|
|
||||||
mod commands;
|
|
||||||
mod lsp_ext;
|
|
||||||
mod dfs;
|
|
||||||
mod merge_views;
|
|
||||||
mod consts;
|
|
||||||
mod opengl;
|
|
||||||
mod url_norm;
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod test;
|
|
||||||
|
|
||||||
lazy_static! {
|
|
||||||
static ref RE_DIAGNOSTIC: Regex = Regex::new(r#"^(?P<filepath>[^?<>*|"]+)\((?P<linenum>\d+)\) : (?P<severity>error|warning) [A-C]\d+: (?P<output>.+)"#).unwrap();
|
|
||||||
static ref RE_VERSION: Regex = Regex::new(r#"#version [\d]{3}"#).unwrap();
|
|
||||||
static ref RE_INCLUDE: Regex = Regex::new(r#"^(?:\s)*?(?:#include) "(.+)"\r?"#).unwrap();
|
|
||||||
static ref RE_INCLUDE_EXTENSION: Regex = Regex::new(r#"#extension GL_GOOGLE_include_directive ?: ?require"#).unwrap();
|
|
||||||
}
|
|
||||||
|
|
||||||
fn main() {
|
|
||||||
let stdin = stdin();
|
|
||||||
|
|
||||||
let endpoint_output = LSPEndpoint::create_lsp_output_with_output_stream(stdout);
|
|
||||||
|
|
||||||
let cache_graph = graph::CachedStableGraph::new();
|
|
||||||
|
|
||||||
let mut langserver = MinecraftShaderLanguageServer {
|
|
||||||
endpoint: endpoint_output.clone(),
|
|
||||||
graph: Rc::new(RefCell::new(cache_graph)),
|
|
||||||
wait: WaitGroup::new(),
|
|
||||||
root: "".into(),
|
|
||||||
command_provider: None,
|
|
||||||
opengl_context: Rc::new(opengl::OpenGLContext::new())
|
|
||||||
};
|
|
||||||
|
|
||||||
langserver.command_provider = Some(commands::CustomCommandProvider::new(vec![
|
|
||||||
(
|
|
||||||
"graphDot",
|
|
||||||
Box::new(commands::GraphDotCommand {
|
|
||||||
graph: Rc::clone(&langserver.graph),
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"virtualMerge",
|
|
||||||
Box::new(commands::VirtualMergedDocument{
|
|
||||||
graph: Rc::clone(&langserver.graph)
|
|
||||||
})
|
|
||||||
)
|
|
||||||
]));
|
|
||||||
|
|
||||||
LSPEndpoint::run_server_from_input(&mut stdin.lock(), endpoint_output, langserver);
|
|
||||||
}
|
|
||||||
|
|
||||||
struct MinecraftShaderLanguageServer {
|
|
||||||
endpoint: Endpoint,
|
|
||||||
graph: Rc<RefCell<graph::CachedStableGraph>>,
|
|
||||||
wait: WaitGroup,
|
|
||||||
root: PathBuf,
|
|
||||||
command_provider: Option<commands::CustomCommandProvider>,
|
|
||||||
opengl_context: Rc<dyn opengl::ShaderValidator>
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, PartialEq, Eq, Hash)]
|
|
||||||
pub struct IncludePosition {
|
|
||||||
line: usize,
|
|
||||||
start: usize,
|
|
||||||
end: usize,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Debug for IncludePosition {
|
|
||||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
|
||||||
write!(f, "{{line: {}}}", self.line)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Display for IncludePosition {
|
|
||||||
fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), std::fmt::Error> {
|
|
||||||
write!(f, "{{line: {}}}", self.line)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub enum TreeType {
|
|
||||||
Fragment, Vertex, Geometry
|
|
||||||
}
|
|
||||||
|
|
||||||
impl MinecraftShaderLanguageServer {
|
|
||||||
pub fn error_not_available<DATA>(data: DATA) -> MethodError<DATA> {
|
|
||||||
let msg = "Functionality not implemented.".to_string();
|
|
||||||
MethodError::<DATA> {
|
|
||||||
code: 1,
|
|
||||||
message: msg,
|
|
||||||
data,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn gen_initial_graph(&self) {
|
|
||||||
eprintln!("root of project is {:?}", self.root);
|
|
||||||
|
|
||||||
// filter directories and files not ending in any of the 3 extensions
|
|
||||||
WalkDir::new(&self.root).into_iter().filter_map(|entry| {
|
|
||||||
if entry.is_err() {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
|
|
||||||
let entry = entry.unwrap();
|
|
||||||
let path = entry.path();
|
|
||||||
if path.is_dir() {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
|
|
||||||
let ext = match path.extension() {
|
|
||||||
Some(e) => e,
|
|
||||||
None => return None,
|
|
||||||
};
|
|
||||||
|
|
||||||
if ext != "vsh" && ext != "fsh" && ext != "glsl" && ext != "inc" {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
|
|
||||||
Some(entry.into_path())
|
|
||||||
}).for_each(|path| {
|
|
||||||
// iterate all valid found files, search for includes, add a node into the graph for each
|
|
||||||
// file and add a file->includes KV into the map
|
|
||||||
self.add_file_and_includes_to_graph(&path);
|
|
||||||
});
|
|
||||||
|
|
||||||
eprintln!("finished building project include graph");
|
|
||||||
}
|
|
||||||
|
|
||||||
fn add_file_and_includes_to_graph(&self, path: &PathBuf) {
|
|
||||||
let includes = self.find_includes(path);
|
|
||||||
|
|
||||||
let idx = self.graph.borrow_mut().add_node(&path);
|
|
||||||
|
|
||||||
//eprintln!("adding {:?} with {:?}", path, includes);
|
|
||||||
for include in includes {
|
|
||||||
self.add_include(include, idx);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn add_include(&self, include: (PathBuf, IncludePosition), node: NodeIndex) {
|
|
||||||
let child = self.graph.borrow_mut().add_node(&include.0);
|
|
||||||
self.graph.borrow_mut().add_edge(node, child, include.1);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn find_includes(&self, file: &PathBuf) -> Vec<(PathBuf, IncludePosition)> {
|
|
||||||
let mut includes = Vec::default();
|
|
||||||
|
|
||||||
let buf = BufReader::new(std::fs::File::open(file).unwrap());
|
|
||||||
buf.lines()
|
|
||||||
.enumerate()
|
|
||||||
.filter_map(|line| match line.1 {
|
|
||||||
Ok(t) => Some((line.0, t)),
|
|
||||||
Err(_e) => None,
|
|
||||||
})
|
|
||||||
.filter(|line| RE_INCLUDE.is_match(line.1.as_str()))
|
|
||||||
.for_each(|line| {
|
|
||||||
let cap = RE_INCLUDE
|
|
||||||
.captures(line.1.as_str())
|
|
||||||
.unwrap()
|
|
||||||
.get(1)
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
let start = cap.start();
|
|
||||||
let end = cap.end();
|
|
||||||
let mut path: String = cap.as_str().into();
|
|
||||||
|
|
||||||
// TODO: difference between / and not
|
|
||||||
let full_include = if path.starts_with('/') {
|
|
||||||
path = path.strip_prefix('/').unwrap().to_string();
|
|
||||||
self.root.join("shaders").join(PathBuf::from_slash(&path))
|
|
||||||
} else {
|
|
||||||
file.parent().unwrap().join(PathBuf::from_slash(&path))
|
|
||||||
};
|
|
||||||
|
|
||||||
includes.push((
|
|
||||||
full_include,
|
|
||||||
IncludePosition {
|
|
||||||
line: line.0,
|
|
||||||
start,
|
|
||||||
end,
|
|
||||||
}
|
|
||||||
));
|
|
||||||
});
|
|
||||||
|
|
||||||
includes
|
|
||||||
}
|
|
||||||
|
|
||||||
fn update_includes(&self, file: &PathBuf) {
|
|
||||||
let includes = self.find_includes(file);
|
|
||||||
|
|
||||||
eprintln!("updating {:?} with {:?}", file, includes);
|
|
||||||
|
|
||||||
let idx = match self.graph.borrow_mut().find_node(&file) {
|
|
||||||
None => {
|
|
||||||
return
|
|
||||||
},
|
|
||||||
Some(n) => n,
|
|
||||||
};
|
|
||||||
|
|
||||||
let prev_children: HashSet<_, RandomState> = HashSet::from_iter(self.graph.borrow().child_node_meta(idx));
|
|
||||||
let new_children: HashSet<_, RandomState> = HashSet::from_iter(includes.iter().map(|e| e.clone()));
|
|
||||||
|
|
||||||
let to_be_added = new_children.difference(&prev_children);
|
|
||||||
let to_be_removed = prev_children.difference(&new_children);
|
|
||||||
|
|
||||||
eprintln!("removing:\n\t{:?}\nadding:\n\t{:?}", to_be_removed, to_be_added);
|
|
||||||
|
|
||||||
for removal in to_be_removed {
|
|
||||||
let child = self.graph.borrow_mut().find_node(&removal.0).unwrap();
|
|
||||||
self.graph.borrow_mut().remove_edge(idx, child);
|
|
||||||
}
|
|
||||||
|
|
||||||
for insertion in to_be_added {
|
|
||||||
self.add_include(includes.iter().find(|f| f.0 == *insertion.0).unwrap().clone(), idx);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn lint(&self, uri: &PathBuf) -> Result<HashMap<Url, Vec<Diagnostic>>> {
|
|
||||||
// get all top level ancestors of this file
|
|
||||||
let file_ancestors = match self.get_file_toplevel_ancestors(uri) {
|
|
||||||
Ok(opt) => match opt {
|
|
||||||
Some(ancestors) => ancestors,
|
|
||||||
None => vec![],
|
|
||||||
},
|
|
||||||
Err(e) => return Err(e),
|
|
||||||
};
|
|
||||||
|
|
||||||
eprintln!("ancestors for {:?}:\n\t{:?}", uri, file_ancestors.iter().map(|e| PathBuf::from_str(&self.graph.borrow().graph.node_weight(*e).unwrap().clone()).unwrap()).collect::<Vec<PathBuf>>());
|
|
||||||
|
|
||||||
// the set of all filepath->content. TODO: change to Url?
|
|
||||||
let mut all_sources: HashMap<PathBuf, String> = HashMap::new();
|
|
||||||
// the set of filepath->list of diagnostics to report
|
|
||||||
let mut diagnostics: HashMap<Url, Vec<Diagnostic>> = HashMap::new();
|
|
||||||
|
|
||||||
// we want to backfill the diagnostics map with all linked sources
|
|
||||||
let back_fill = |all_sources, diagnostics: &mut HashMap<Url, Vec<Diagnostic>>| {
|
|
||||||
for (path, _) in all_sources {
|
|
||||||
diagnostics.entry(Url::from_file_path(path).unwrap()).or_default();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// if we are a top-level file (this has to be one of the set defined by Optifine, right?)
|
|
||||||
if file_ancestors.is_empty() {
|
|
||||||
// gather the list of all descendants
|
|
||||||
let root = self.graph.borrow_mut().find_node(&uri).unwrap();
|
|
||||||
let tree = match self.get_dfs_for_node(root) {
|
|
||||||
Ok(tree) => tree,
|
|
||||||
Err(e) => {
|
|
||||||
diagnostics.insert(Url::from_file_path(uri).unwrap(), vec![e.into()]);
|
|
||||||
return Ok(diagnostics);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
all_sources.extend( self.load_sources(&tree)?);
|
|
||||||
|
|
||||||
let view = {
|
|
||||||
let graph = self.graph.borrow();
|
|
||||||
merge_views::generate_merge_list(&tree, &all_sources, &graph)
|
|
||||||
};
|
|
||||||
|
|
||||||
let root_path = self.graph.borrow().get_node(root).clone();
|
|
||||||
let tree_type = if root_path.extension().unwrap() == "fsh" {
|
|
||||||
TreeType::Fragment
|
|
||||||
} else if root_path.extension().unwrap() == "vsh" {
|
|
||||||
TreeType::Vertex
|
|
||||||
} else if root_path.extension().unwrap() == "gsh" {
|
|
||||||
TreeType::Geometry
|
|
||||||
} else {
|
|
||||||
eprintln!("got a non fsh|vsh ({:?}) as a file root ancestor: {:?}", root_path.extension().unwrap(), root_path);
|
|
||||||
back_fill(&all_sources, &mut diagnostics);
|
|
||||||
return Ok(diagnostics)
|
|
||||||
};
|
|
||||||
|
|
||||||
let stdout = match self.opengl_context.clone().validate(tree_type, view) {
|
|
||||||
Some(s) => s,
|
|
||||||
None => {
|
|
||||||
back_fill(&all_sources, &mut diagnostics);
|
|
||||||
return Ok(diagnostics)
|
|
||||||
},
|
|
||||||
};
|
|
||||||
diagnostics.extend(self.parse_validator_stdout(uri, stdout, ""));
|
|
||||||
} else {
|
|
||||||
let mut all_trees: Vec<(TreeType, Vec<(NodeIndex, Option<_>)>)> = Vec::new();
|
|
||||||
|
|
||||||
for root in &file_ancestors {
|
|
||||||
let nodes = match self.get_dfs_for_node(*root) {
|
|
||||||
Ok(nodes) => nodes,
|
|
||||||
Err(e) => {
|
|
||||||
diagnostics.insert(Url::from_file_path(uri).unwrap(), vec![e.into()]);
|
|
||||||
back_fill(&all_sources, &mut diagnostics); // TODO: confirm
|
|
||||||
return Ok(diagnostics);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let root_path = self.graph.borrow().get_node(*root).clone();
|
|
||||||
let tree_type = if root_path.extension().unwrap() == "fsh" {
|
|
||||||
TreeType::Fragment
|
|
||||||
} else if root_path.extension().unwrap() == "vsh" {
|
|
||||||
TreeType::Vertex
|
|
||||||
} else if root_path.extension().unwrap() == "gsh" {
|
|
||||||
TreeType::Geometry
|
|
||||||
} else {
|
|
||||||
eprintln!("got a non fsh|vsh ({:?}) as a file root ancestor: {:?}", root_path.extension().unwrap(), root_path);
|
|
||||||
continue;
|
|
||||||
};
|
|
||||||
|
|
||||||
let sources = self.load_sources(&nodes)?;
|
|
||||||
all_trees.push((tree_type, nodes));
|
|
||||||
all_sources.extend(sources);
|
|
||||||
}
|
|
||||||
|
|
||||||
for tree in all_trees {
|
|
||||||
let view = {
|
|
||||||
let graph = self.graph.borrow();
|
|
||||||
merge_views::generate_merge_list(&tree.1, &all_sources, &graph)
|
|
||||||
};
|
|
||||||
|
|
||||||
let stdout = match self.opengl_context.clone().validate(tree.0, view) {
|
|
||||||
Some(s) => s,
|
|
||||||
None => continue,
|
|
||||||
};
|
|
||||||
diagnostics.extend(self.parse_validator_stdout(uri, stdout, ""));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
back_fill(&all_sources, &mut diagnostics);
|
|
||||||
Ok(diagnostics)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn parse_validator_stdout(&self, uri: &PathBuf, stdout: String, _source: &str) -> HashMap<Url, Vec<Diagnostic>> {
|
|
||||||
let stdout_lines = stdout.split('\n');
|
|
||||||
let mut diagnostics: HashMap<Url, Vec<Diagnostic>> = HashMap::with_capacity(stdout_lines.count());
|
|
||||||
let stdout_lines = stdout.split('\n');
|
|
||||||
|
|
||||||
for line in stdout_lines {
|
|
||||||
let diagnostic_capture = match RE_DIAGNOSTIC.captures(line) {
|
|
||||||
Some(d) => d,
|
|
||||||
None => continue
|
|
||||||
};
|
|
||||||
|
|
||||||
eprintln!("match {:?}", diagnostic_capture);
|
|
||||||
|
|
||||||
let msg = diagnostic_capture.name("output").unwrap().as_str();
|
|
||||||
|
|
||||||
let line = match diagnostic_capture.name("linenum") {
|
|
||||||
Some(c) => match c.as_str().parse::<u32>() {
|
|
||||||
Ok(i) => i,
|
|
||||||
Err(_) => 0,
|
|
||||||
},
|
|
||||||
None => 0,
|
|
||||||
} - 2;
|
|
||||||
|
|
||||||
// TODO: line matching maybe
|
|
||||||
/* let line_text = source_lines[line as usize];
|
|
||||||
let leading_whitespace = line_text.len() - line_text.trim_start().len(); */
|
|
||||||
|
|
||||||
let severity = match diagnostic_capture.name("severity") {
|
|
||||||
Some(c) => match c.as_str() {
|
|
||||||
"error" => DiagnosticSeverity::Error,
|
|
||||||
"warning" => DiagnosticSeverity::Warning,
|
|
||||||
_ => DiagnosticSeverity::Information,
|
|
||||||
}
|
|
||||||
_ => DiagnosticSeverity::Information,
|
|
||||||
};
|
|
||||||
|
|
||||||
let origin = match diagnostic_capture.name("filepath") {
|
|
||||||
Some(o) => {
|
|
||||||
if o.as_str().to_string() == "0" {
|
|
||||||
uri.to_str().unwrap().to_string()
|
|
||||||
} else {
|
|
||||||
o.as_str().to_string()
|
|
||||||
}
|
|
||||||
},
|
|
||||||
None => uri.to_str().unwrap().to_string(),
|
|
||||||
};
|
|
||||||
|
|
||||||
let diagnostic = Diagnostic {
|
|
||||||
range: Range::new(
|
|
||||||
/* Position::new(line, leading_whitespace as u64),
|
|
||||||
Position::new(line, line_text.len() as u64) */
|
|
||||||
Position::new(line, 0),
|
|
||||||
Position::new(line, 1000),
|
|
||||||
),
|
|
||||||
code: None,
|
|
||||||
severity: Some(severity),
|
|
||||||
source: Some(consts::SOURCE.into()),
|
|
||||||
message: msg.trim().into(),
|
|
||||||
related_information: None,
|
|
||||||
tags: None,
|
|
||||||
code_description: Option::None,
|
|
||||||
data: Option::None,
|
|
||||||
};
|
|
||||||
|
|
||||||
let origin_url = Url::from_file_path(origin).unwrap();
|
|
||||||
match diagnostics.get_mut(&origin_url) {
|
|
||||||
Some(d) => d.push(diagnostic),
|
|
||||||
None => {
|
|
||||||
diagnostics.insert(origin_url, vec![diagnostic]);
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
diagnostics
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_dfs_for_node(&self, root: NodeIndex) -> Result<Vec<(NodeIndex, Option<NodeIndex>)>, dfs::error::CycleError> {
|
|
||||||
let graph_ref = self.graph.borrow();
|
|
||||||
|
|
||||||
let dfs = dfs::Dfs::new(&graph_ref, root);
|
|
||||||
|
|
||||||
dfs.collect::<Result<Vec<_>, _>>()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn load_sources(&self, nodes: &[(NodeIndex, Option<NodeIndex>)]) -> Result<HashMap<PathBuf, String>> {
|
|
||||||
let mut sources = HashMap::new();
|
|
||||||
|
|
||||||
for node in nodes {
|
|
||||||
let graph = self.graph.borrow();
|
|
||||||
let path = graph.get_node(node.0);
|
|
||||||
|
|
||||||
if sources.contains_key(&path) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
let source = match fs::read_to_string(&path) {
|
|
||||||
Ok(s) => s,
|
|
||||||
Err(e) => return Err(anyhow!("error reading {:?}: {}", path, e))
|
|
||||||
};
|
|
||||||
sources.insert(path.clone(), source);
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(sources)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_file_toplevel_ancestors(&self, uri: &PathBuf) -> Result<Option<Vec<petgraph::stable_graph::NodeIndex>>> {
|
|
||||||
let curr_node = match self.graph.borrow_mut().find_node(uri) {
|
|
||||||
Some(n) => n,
|
|
||||||
None => return Err(anyhow!("node not found {:?}", uri)),
|
|
||||||
};
|
|
||||||
let roots = self.graph.borrow().collect_root_ancestors(curr_node);
|
|
||||||
if roots.is_empty() {
|
|
||||||
return Ok(None);
|
|
||||||
}
|
|
||||||
Ok(Some(roots))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn publish_diagnostic(&self, diagnostics: HashMap<Url, Vec<Diagnostic>>, document_version: Option<i32>) {
|
|
||||||
eprintln!("DIAGNOSTICS:\n{:?}", diagnostics);
|
|
||||||
for (uri, diagnostics) in diagnostics {
|
|
||||||
self.endpoint.send_notification(PublishDiagnostics::METHOD, PublishDiagnosticsParams {
|
|
||||||
uri,
|
|
||||||
diagnostics,
|
|
||||||
version: document_version,
|
|
||||||
}).expect("failed to publish diagnostics");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn set_status(&self, status: impl Into<String>, message: impl Into<String>, icon: impl Into<String>) {
|
|
||||||
self.endpoint.send_notification(lsp_ext::Status::METHOD, lsp_ext::StatusParams {
|
|
||||||
status: status.into(),
|
|
||||||
message: Some(message.into()),
|
|
||||||
icon: Some(icon.into()),
|
|
||||||
}).unwrap_or(());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl LanguageServerHandling for MinecraftShaderLanguageServer {
|
|
||||||
fn initialize(&mut self, params: InitializeParams, completable: MethodCompletable<InitializeResult, InitializeError>) {
|
|
||||||
self.wait.add(1);
|
|
||||||
|
|
||||||
let mut capabilities = ServerCapabilities::default();
|
|
||||||
capabilities.hover_provider = None;
|
|
||||||
capabilities.document_link_provider = Some(DocumentLinkOptions {
|
|
||||||
resolve_provider: None,
|
|
||||||
work_done_progress_options: WorkDoneProgressOptions {
|
|
||||||
work_done_progress: None,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
capabilities.execute_command_provider = Some(ExecuteCommandOptions {
|
|
||||||
commands: vec!["graphDot".into()],
|
|
||||||
work_done_progress_options: WorkDoneProgressOptions {
|
|
||||||
work_done_progress: None,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
capabilities.text_document_sync = Some(TextDocumentSyncCapability::Options(
|
|
||||||
TextDocumentSyncOptions {
|
|
||||||
open_close: Some(true),
|
|
||||||
will_save: None,
|
|
||||||
will_save_wait_until: None,
|
|
||||||
change: Some(TextDocumentSyncKind::Full),
|
|
||||||
save: Some(TextDocumentSyncSaveOptions::SaveOptions(SaveOptions {
|
|
||||||
include_text: Some(true),
|
|
||||||
}))
|
|
||||||
},
|
|
||||||
));
|
|
||||||
|
|
||||||
let root = match params.root_uri {
|
|
||||||
Some(uri) => PathBuf::from_url(uri),
|
|
||||||
None => {
|
|
||||||
completable.complete(Err(MethodError {
|
|
||||||
code: 42069,
|
|
||||||
message: "Must be in workspace".into(),
|
|
||||||
data: InitializeError {
|
|
||||||
retry: false,
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
completable.complete(Ok(InitializeResult {
|
|
||||||
capabilities,
|
|
||||||
server_info: None,
|
|
||||||
}));
|
|
||||||
|
|
||||||
self.set_status("loading", "Building dependency graph...", "$(loading~spin)");
|
|
||||||
|
|
||||||
self.root = root;
|
|
||||||
|
|
||||||
self.gen_initial_graph();
|
|
||||||
|
|
||||||
self.set_status("ready", "Project initialized", "$(check)");
|
|
||||||
}
|
|
||||||
|
|
||||||
fn shutdown(&mut self, _: (), completable: LSCompletable<()>) {
|
|
||||||
eprintln!("shutting down language server...");
|
|
||||||
completable.complete(Ok(()));
|
|
||||||
}
|
|
||||||
|
|
||||||
fn exit(&mut self, _: ()) {
|
|
||||||
self.endpoint.request_shutdown();
|
|
||||||
}
|
|
||||||
|
|
||||||
fn workspace_change_configuration(&mut self, params: DidChangeConfigurationParams) {
|
|
||||||
//let config = params.settings.as_object().unwrap().get("mcglsl").unwrap();
|
|
||||||
|
|
||||||
eprintln!("{:?}", params.settings.as_object().unwrap());
|
|
||||||
|
|
||||||
self.wait.done();
|
|
||||||
}
|
|
||||||
|
|
||||||
fn did_open_text_document(&mut self, params: DidOpenTextDocumentParams) {
|
|
||||||
//eprintln!("opened doc {}", params.text_document.uri);
|
|
||||||
let path = PathBuf::from_url(params.text_document.uri);
|
|
||||||
if self.graph.borrow_mut().find_node(&path) == None {
|
|
||||||
self.add_file_and_includes_to_graph(&path);
|
|
||||||
}
|
|
||||||
match self.lint(&path) {
|
|
||||||
Ok(diagnostics) => self.publish_diagnostic(diagnostics, None),
|
|
||||||
Err(e) => eprintln!("error linting: {}", e),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn did_change_text_document(&mut self, _: DidChangeTextDocumentParams) {}
|
|
||||||
|
|
||||||
fn did_close_text_document(&mut self, _: DidCloseTextDocumentParams) {}
|
|
||||||
|
|
||||||
fn did_save_text_document(&mut self, params: DidSaveTextDocumentParams) {
|
|
||||||
eprintln!("saved doc {}", params.text_document.uri);
|
|
||||||
|
|
||||||
let path = PathBuf::from_url(params.text_document.uri);
|
|
||||||
self.update_includes(&path);
|
|
||||||
|
|
||||||
match self.lint(&path) {
|
|
||||||
Ok(diagnostics) => self.publish_diagnostic(diagnostics, None),
|
|
||||||
Err(e) => eprintln!("error linting: {}", e),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn did_change_watched_files(&mut self, _: DidChangeWatchedFilesParams) {}
|
|
||||||
|
|
||||||
fn completion(&mut self, _: TextDocumentPositionParams, completable: LSCompletable<CompletionList>) {
|
|
||||||
completable.complete(Err(Self::error_not_available(())));
|
|
||||||
}
|
|
||||||
|
|
||||||
fn resolve_completion_item(&mut self, _: CompletionItem, completable: LSCompletable<CompletionItem>) {
|
|
||||||
completable.complete(Err(Self::error_not_available(())));
|
|
||||||
}
|
|
||||||
|
|
||||||
fn hover(&mut self, _: TextDocumentPositionParams, _: LSCompletable<Hover>) {
|
|
||||||
self.wait.wait();
|
|
||||||
/* completable.complete(Ok(Hover{
|
|
||||||
contents: HoverContents::Markup(MarkupContent{
|
|
||||||
kind: MarkupKind::Markdown,
|
|
||||||
value: String::from("# Hello World"),
|
|
||||||
}),
|
|
||||||
range: None,
|
|
||||||
})); */
|
|
||||||
}
|
|
||||||
|
|
||||||
fn execute_command(&mut self, params: ExecuteCommandParams, completable: LSCompletable<Option<Value>>) {
|
|
||||||
match self.command_provider.as_ref().unwrap().execute(¶ms.command, params.arguments, &self.root) {
|
|
||||||
Ok(resp) => {
|
|
||||||
eprintln!("executed {} successfully", params.command);
|
|
||||||
self.endpoint.send_notification(ShowMessage::METHOD, ShowMessageParams {
|
|
||||||
typ: MessageType::Info,
|
|
||||||
message: format!("Command {} executed successfully.", params.command),
|
|
||||||
}).expect("failed to send popup/show message notification");
|
|
||||||
completable.complete(Ok(Some(resp)))
|
|
||||||
},
|
|
||||||
Err(err) => {
|
|
||||||
self.endpoint.send_notification(ShowMessage::METHOD, ShowMessageParams {
|
|
||||||
typ: MessageType::Error,
|
|
||||||
message: format!("Failed to execute `{}`. Reason: {}", params.command, err),
|
|
||||||
}).expect("failed to send popup/show message notification");
|
|
||||||
eprintln!("failed to execute {}: {}", params.command, err);
|
|
||||||
completable.complete(Err(MethodError::new(32420, err.to_string(), ())))
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn signature_help(&mut self, _: TextDocumentPositionParams, completable: LSCompletable<SignatureHelp>) {
|
|
||||||
completable.complete(Err(Self::error_not_available(())));
|
|
||||||
}
|
|
||||||
|
|
||||||
fn goto_definition(&mut self, _: TextDocumentPositionParams, completable: LSCompletable<Vec<Location>>) {
|
|
||||||
completable.complete(Err(Self::error_not_available(())));
|
|
||||||
}
|
|
||||||
|
|
||||||
fn references(&mut self, _: ReferenceParams, completable: LSCompletable<Vec<Location>>) {
|
|
||||||
completable.complete(Err(Self::error_not_available(())));
|
|
||||||
}
|
|
||||||
|
|
||||||
fn document_highlight(&mut self, _: TextDocumentPositionParams, completable: LSCompletable<Vec<DocumentHighlight>>) {
|
|
||||||
completable.complete(Err(Self::error_not_available(())));
|
|
||||||
}
|
|
||||||
|
|
||||||
fn document_symbols(&mut self, _: DocumentSymbolParams, completable: LSCompletable<Vec<SymbolInformation>>) {
|
|
||||||
completable.complete(Err(Self::error_not_available(())));
|
|
||||||
}
|
|
||||||
|
|
||||||
fn workspace_symbols(&mut self, _: WorkspaceSymbolParams, completable: LSCompletable<Vec<SymbolInformation>>) {
|
|
||||||
completable.complete(Err(Self::error_not_available(())));
|
|
||||||
}
|
|
||||||
|
|
||||||
fn code_action(&mut self, _: CodeActionParams, completable: LSCompletable<Vec<Command>>) {
|
|
||||||
completable.complete(Err(Self::error_not_available(())));
|
|
||||||
}
|
|
||||||
|
|
||||||
fn code_lens(&mut self, _: CodeLensParams, completable: LSCompletable<Vec<CodeLens>>) {
|
|
||||||
completable.complete(Err(Self::error_not_available(())));
|
|
||||||
}
|
|
||||||
|
|
||||||
fn code_lens_resolve(&mut self, _: CodeLens, completable: LSCompletable<CodeLens>) {
|
|
||||||
completable.complete(Err(Self::error_not_available(())));
|
|
||||||
}
|
|
||||||
|
|
||||||
fn document_link(&mut self, params: DocumentLinkParams, completable: LSCompletable<Vec<DocumentLink>>) {
|
|
||||||
eprintln!("document link file: {:?}", params.text_document.uri.to_file_path().unwrap());
|
|
||||||
// node for current document
|
|
||||||
let curr_doc = params
|
|
||||||
.text_document
|
|
||||||
.uri
|
|
||||||
.to_file_path()
|
|
||||||
.unwrap();
|
|
||||||
let node = match self.graph.borrow_mut().find_node(&curr_doc) {
|
|
||||||
Some(n) => n,
|
|
||||||
None => {
|
|
||||||
completable.complete(Ok(vec![]));
|
|
||||||
return
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
let edges: Vec<DocumentLink> = self
|
|
||||||
.graph
|
|
||||||
.borrow()
|
|
||||||
.child_node_indexes(node)
|
|
||||||
.into_iter()
|
|
||||||
.filter_map(|child| {
|
|
||||||
let graph = self.graph.borrow();
|
|
||||||
let value = graph.get_edge_meta(node, child);
|
|
||||||
let path = graph.get_node(child);
|
|
||||||
let url = match Url::from_file_path(&path) {
|
|
||||||
Ok(url) => url,
|
|
||||||
Err(e) => {
|
|
||||||
eprintln!("error converting {:?} into url: {:?}", path, e);
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
Some(DocumentLink {
|
|
||||||
range: Range::new(
|
|
||||||
Position::new(
|
|
||||||
u32::try_from(value.line).unwrap(),
|
|
||||||
u32::try_from(value.start).unwrap()),
|
|
||||||
Position::new(
|
|
||||||
u32::try_from(value.line).unwrap(),
|
|
||||||
u32::try_from(value.end).unwrap()),
|
|
||||||
),
|
|
||||||
target: Some(url),
|
|
||||||
//tooltip: Some(url.path().to_string().strip_prefix(self.root.clone().unwrap().as_str()).unwrap().to_string()),
|
|
||||||
tooltip: None,
|
|
||||||
data: None,
|
|
||||||
})
|
|
||||||
}).collect();
|
|
||||||
eprintln!("links: {:?}", edges);
|
|
||||||
completable.complete(Ok(edges));
|
|
||||||
}
|
|
||||||
|
|
||||||
fn document_link_resolve(&mut self, _: DocumentLink, completable: LSCompletable<DocumentLink>) {
|
|
||||||
completable.complete(Err(Self::error_not_available(())));
|
|
||||||
}
|
|
||||||
|
|
||||||
fn formatting(&mut self, _: DocumentFormattingParams, completable: LSCompletable<Vec<TextEdit>>) {
|
|
||||||
completable.complete(Err(Self::error_not_available(())));
|
|
||||||
}
|
|
||||||
|
|
||||||
fn range_formatting(&mut self, _: DocumentRangeFormattingParams, completable: LSCompletable<Vec<TextEdit>>) {
|
|
||||||
completable.complete(Err(Self::error_not_available(())));
|
|
||||||
}
|
|
||||||
|
|
||||||
fn on_type_formatting(&mut self, _: DocumentOnTypeFormattingParams, completable: LSCompletable<Vec<TextEdit>>) {
|
|
||||||
completable.complete(Err(Self::error_not_available(())));
|
|
||||||
}
|
|
||||||
|
|
||||||
fn rename(&mut self, _: RenameParams, completable: LSCompletable<WorkspaceEdit>) {
|
|
||||||
completable.complete(Err(Self::error_not_available(())));
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,208 +0,0 @@
|
||||||
use std::{collections::{HashMap, LinkedList, VecDeque}, path::PathBuf};
|
|
||||||
use std::iter::Peekable;
|
|
||||||
use std::cmp::min;
|
|
||||||
|
|
||||||
use core::slice::Iter;
|
|
||||||
|
|
||||||
use petgraph::stable_graph::NodeIndex;
|
|
||||||
|
|
||||||
use crate::graph::CachedStableGraph;
|
|
||||||
|
|
||||||
pub fn generate_merge_list<'a>(
|
|
||||||
nodes: &'a [(NodeIndex, Option<NodeIndex>)],
|
|
||||||
sources: &'a HashMap<PathBuf, String>,
|
|
||||||
graph: &'a CachedStableGraph
|
|
||||||
) -> String {
|
|
||||||
let mut line_directives: Vec<String> = Vec::new();
|
|
||||||
|
|
||||||
// list of source code views onto the below sources
|
|
||||||
let mut merge_list: LinkedList<&'a str> = LinkedList::new();
|
|
||||||
|
|
||||||
line_directives.reserve(nodes.len() * 2);
|
|
||||||
|
|
||||||
let mut last_offset_set: HashMap<PathBuf, usize> = HashMap::new();
|
|
||||||
|
|
||||||
let mut nodes_iter = nodes.iter().peekable();
|
|
||||||
|
|
||||||
let first = nodes_iter.next().unwrap().0;
|
|
||||||
let first_path = graph.get_node(first).clone();
|
|
||||||
|
|
||||||
last_offset_set.insert(first_path.clone(), 0);
|
|
||||||
|
|
||||||
let line_ending_offset = if is_crlf(sources.get(&first_path).unwrap()) {
|
|
||||||
2
|
|
||||||
} else {
|
|
||||||
1
|
|
||||||
};
|
|
||||||
|
|
||||||
// stack to keep track of the depth first traversal
|
|
||||||
let mut stack = VecDeque::<NodeIndex>::new();
|
|
||||||
|
|
||||||
create_merge_views(&mut nodes_iter, &mut merge_list, &mut last_offset_set, graph, sources, &mut line_directives, &mut stack, line_ending_offset);
|
|
||||||
|
|
||||||
// now we add a view of the remainder of the root file
|
|
||||||
let offset = *last_offset_set.get(&first_path).unwrap();
|
|
||||||
|
|
||||||
let len = sources.get(&first_path).unwrap().len();
|
|
||||||
merge_list.push_back(&sources.get(&first_path).unwrap()[min(offset, len) ..]);
|
|
||||||
|
|
||||||
let total_len = merge_list.iter().fold(0, |a, b| {
|
|
||||||
a + b.len()
|
|
||||||
});
|
|
||||||
|
|
||||||
let mut merged = String::with_capacity(total_len);
|
|
||||||
for slice in merge_list {
|
|
||||||
merged.push_str(slice);
|
|
||||||
}
|
|
||||||
|
|
||||||
merged
|
|
||||||
}
|
|
||||||
|
|
||||||
fn is_crlf(source: &String) -> bool {
|
|
||||||
source.contains("\r\n")
|
|
||||||
}
|
|
||||||
|
|
||||||
fn create_merge_views<'a>(
|
|
||||||
nodes: &mut Peekable<Iter<(NodeIndex, Option<NodeIndex>)>>,
|
|
||||||
merge_list: &mut LinkedList<&'a str>,
|
|
||||||
last_offset_set: &mut HashMap<PathBuf, usize>,
|
|
||||||
graph: &'a CachedStableGraph,
|
|
||||||
sources: &'a HashMap<PathBuf, String>,
|
|
||||||
line_directives: &mut Vec<String>,
|
|
||||||
stack: &mut VecDeque<NodeIndex>,
|
|
||||||
line_ending_offset: usize,
|
|
||||||
) {
|
|
||||||
|
|
||||||
loop {
|
|
||||||
let n = match nodes.next() {
|
|
||||||
Some(n) => n,
|
|
||||||
None => return,
|
|
||||||
};
|
|
||||||
|
|
||||||
let parent = n.1.unwrap();
|
|
||||||
let child = n.0;
|
|
||||||
let edge = graph.get_edge_meta(parent, child);
|
|
||||||
let parent_path = graph.get_node(parent).clone();
|
|
||||||
let child_path = graph.get_node(child).clone();
|
|
||||||
|
|
||||||
let parent_source = sources.get(&parent_path).unwrap();
|
|
||||||
let (char_for_line, char_following_line) = char_offset_for_line(edge.line, parent_source, line_ending_offset);
|
|
||||||
|
|
||||||
let offset = *last_offset_set.insert(parent_path.clone(), char_following_line).get_or_insert(0);
|
|
||||||
merge_list.push_back(&parent_source[offset..char_for_line]);
|
|
||||||
add_opening_line_directive(&child_path, merge_list, line_directives);
|
|
||||||
|
|
||||||
match nodes.peek() {
|
|
||||||
Some(next) => {
|
|
||||||
let next = *next;
|
|
||||||
// if the next pair's parent is not a child of the current pair, we dump the rest of this childs source
|
|
||||||
if next.1.unwrap() != child {
|
|
||||||
let child_source = sources.get(&child_path).unwrap();
|
|
||||||
// if ends in \n\n, we want to exclude the last \n for some reason. Ask optilad
|
|
||||||
let offset = {
|
|
||||||
match child_source.ends_with("\n") {
|
|
||||||
true => child_source.len()-line_ending_offset,
|
|
||||||
false => child_source.len(),
|
|
||||||
}
|
|
||||||
};
|
|
||||||
merge_list.push_back(&child_source[..offset]);
|
|
||||||
last_offset_set.insert(child_path.clone(), 0);
|
|
||||||
// +2 because edge.line is 0 indexed but #line is 1 indexed and references the *following* line
|
|
||||||
add_closing_line_directive(edge.line+2, &parent_path, merge_list, line_directives);
|
|
||||||
// if the next pair's parent is not the current pair's parent, we need to bubble up
|
|
||||||
if stack.contains(&next.1.unwrap()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
stack.push_back(parent);
|
|
||||||
create_merge_views(nodes, merge_list, last_offset_set, graph, sources, line_directives, stack, line_ending_offset);
|
|
||||||
stack.pop_back();
|
|
||||||
|
|
||||||
let offset = *last_offset_set.get(&child_path).unwrap();
|
|
||||||
let child_source = sources.get(&child_path).unwrap();
|
|
||||||
// this evaluates to false once the file contents have been exhausted aka offset = child_source.len() + 1
|
|
||||||
let end_offset = {
|
|
||||||
match child_source.ends_with("\n") {
|
|
||||||
true => line_ending_offset/* child_source.len()-1 */,
|
|
||||||
false => 0/* child_source.len() */,
|
|
||||||
}
|
|
||||||
};
|
|
||||||
if offset < child_source.len()-end_offset {
|
|
||||||
// if ends in \n\n, we want to exclude the last \n for some reason. Ask optilad
|
|
||||||
merge_list.push_back(&child_source[offset../* std::cmp::max( */child_source.len()-end_offset/* , offset) */]);
|
|
||||||
last_offset_set.insert(child_path.clone(), 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
// +2 because edge.line is 0 indexed but #line is 1 indexed and references the *following* line
|
|
||||||
add_closing_line_directive(edge.line+2, &parent_path, merge_list, line_directives);
|
|
||||||
|
|
||||||
// we need to check the next item at the point of original return further down the callstack
|
|
||||||
if nodes.peek().is_some() && stack.contains(&nodes.peek().unwrap().1.unwrap()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
None => {
|
|
||||||
let child_source = sources.get(&child_path).unwrap();
|
|
||||||
// if ends in \n\n, we want to exclude the last \n for some reason. Ask optilad
|
|
||||||
let offset = {
|
|
||||||
match child_source.ends_with("\n") {
|
|
||||||
true => child_source.len()-line_ending_offset,
|
|
||||||
false => child_source.len(),
|
|
||||||
}
|
|
||||||
};
|
|
||||||
merge_list.push_back(&child_source[..offset]);
|
|
||||||
last_offset_set.insert(child_path.clone(), 0);
|
|
||||||
// +2 because edge.line is 0 indexed but #line is 1 indexed and references the *following* line
|
|
||||||
add_closing_line_directive(edge.line+2, &parent_path, merge_list, line_directives);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// returns the character offset + 1 of the end of line number `line` and the character
|
|
||||||
// offset + 1 for the end of the line after the previous one
|
|
||||||
fn char_offset_for_line(line_num: usize, source: &str, line_ending_offset: usize) -> (usize, usize) {
|
|
||||||
let mut char_for_line: usize = 0;
|
|
||||||
let mut char_following_line: usize = 0;
|
|
||||||
for (n, line) in source.lines().enumerate() {
|
|
||||||
if n == line_num {
|
|
||||||
char_following_line += line.len()+line_ending_offset;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
char_for_line += line.len()+line_ending_offset;
|
|
||||||
char_following_line = char_for_line;
|
|
||||||
}
|
|
||||||
(char_for_line, char_following_line)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn add_opening_line_directive(path: &PathBuf, merge_list: &mut LinkedList<&str>, line_directives: &mut Vec<String>) {
|
|
||||||
let line_directive = format!("#line 1 \"{}\"\n", path.to_str().unwrap().replace("\\", "\\\\"));
|
|
||||||
line_directives.push(line_directive);
|
|
||||||
unsafe_get_and_insert(merge_list, line_directives);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn add_closing_line_directive(line: usize, path: &PathBuf, merge_list: &mut LinkedList<&str>, line_directives: &mut Vec<String>) {
|
|
||||||
// Optifine doesn't seem to add a leading newline if the previous line was a #line directive
|
|
||||||
let line_directive = if let Some(l) = merge_list.back() {
|
|
||||||
if l.trim().starts_with("#line") {
|
|
||||||
format!("#line {} \"{}\"\n", line, path.to_str().unwrap().replace("\\", "\\\\"))
|
|
||||||
} else {
|
|
||||||
format!("\n#line {} \"{}\"\n", line, path.to_str().unwrap().replace("\\", "\\\\"))
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
format!("\n#line {} \"{}\"\n", line, path.to_str().unwrap().replace("\\", "\\\\"))
|
|
||||||
};
|
|
||||||
|
|
||||||
line_directives.push(line_directive);
|
|
||||||
unsafe_get_and_insert(merge_list, line_directives);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn unsafe_get_and_insert(merge_list: &mut LinkedList<&str>, line_directives: &Vec<String>) {
|
|
||||||
// :^)
|
|
||||||
unsafe {
|
|
||||||
let vec_ptr_offset = line_directives.as_ptr().add(line_directives.len()-1);
|
|
||||||
merge_list.push_back(&vec_ptr_offset.as_ref().unwrap()[..]);
|
|
||||||
}
|
|
||||||
}
|
|
1177
server/src/test.rs
1177
server/src/test.rs
File diff suppressed because it is too large
Load diff
23
server/testdata/04/final.fsh.merge
vendored
23
server/testdata/04/final.fsh.merge
vendored
|
@ -1,23 +0,0 @@
|
||||||
#version 120
|
|
||||||
|
|
||||||
#line 1 "!!"
|
|
||||||
#line 1 "!!"
|
|
||||||
void stuff1() {
|
|
||||||
|
|
||||||
}
|
|
||||||
#line 2 "!!"
|
|
||||||
#line 1 "!!"
|
|
||||||
void stuff2() {
|
|
||||||
|
|
||||||
}
|
|
||||||
#line 3 "!!"
|
|
||||||
#line 4 "!!"
|
|
||||||
#line 1 "!!"
|
|
||||||
void matrix() {
|
|
||||||
|
|
||||||
}
|
|
||||||
#line 5 "!!"
|
|
||||||
|
|
||||||
void main() {
|
|
||||||
|
|
||||||
}
|
|
Loading…
Add table
Add a link
Reference in a new issue