From 786e09bdcf3c597145e36b3b9ff9d6f5b288a91e Mon Sep 17 00:00:00 2001 From: Noah Santschi-Cooney Date: Sun, 14 Aug 2022 12:13:20 +0100 Subject: [PATCH] wip: restructure --- .vscode/launch.json | 1 + client/package-lock.json | 152 +-- client/package.json | 4 +- client/src/commands.ts | 5 +- client/src/extension.ts | 2 +- client/src/log.ts | 5 +- client/src/lspClient.ts | 57 +- client/src/lspExt.ts | 9 +- logo-min.png | Bin 25081 -> 0 bytes logo-mini.png | Bin 24996 -> 25081 bytes package-lock.json | 12 +- server/Cargo.lock | 802 ++++++++++++++- server/filesystem/Cargo.toml | 19 + server/filesystem/src/lib.rs | 6 + server/filesystem/src/source_norm.rs | 41 + server/filesystem/src/top_levels.rs | 65 ++ server/filesystem/src/url_norm.rs | 151 +++ server/graph/Cargo.toml | 25 + server/graph/src/dfs.rs | 342 +++++++ server/graph/src/graph.rs | 454 +++++++++ server/graph/src/lib.rs | 18 + server/include_merger/Cargo.toml | 29 + server/include_merger/src/consts.rs | 13 + server/include_merger/src/lib.rs | 3 + server/include_merger/src/merge_views.rs | 572 +++++++++++ server/logging/Cargo.toml | 11 +- server/logging/src/lib.rs | 16 +- server/logging_macro/Cargo.toml | 1 + server/logging_macro/src/lib.rs | 26 +- server/main/Cargo.toml | 37 +- server/main/src/commands/graph_dot.rs | 52 - server/main/src/commands/merged_includes.rs | 114 --- server/main/src/commands/mod.rs | 36 - server/main/src/configuration.rs | 4 +- server/main/src/consts.rs | 4 - server/main/src/dfs.rs | 335 ------- server/main/src/graph.rs | 374 ------- server/main/src/lsp_ext.rs | 16 - server/main/src/main.rs | 939 +----------------- server/main/src/merge_views.rs | 645 ------------ server/main/src/navigation.rs | 14 +- server/main/src/test.rs | 281 ------ server/main/src/url_norm.rs | 73 -- server/main/testdata/01/final.fsh.merge | 11 - server/main/testdata/02/final.fsh.merge | 27 - server/main/testdata/03/final.fsh.merge | 23 - server/main/testdata/04/final.fsh.merge | 23 - server/main/testdata/06/final.fsh.merge | 17 - server/opengl/Cargo.toml | 26 + .../src/diagnostics_parser.rs | 122 ++- server/opengl/src/lib.rs | 56 ++ .../src/opengl_context.rs} | 28 +- server/opengl/src/opengl_context_facade.rs | 87 ++ server/server/Cargo.toml | 49 + server/server/src/commands/graph_dot.rs | 37 + server/server/src/commands/merged_includes.rs | 42 + server/server/src/commands/mod.rs | 3 + .../src/commands/parse_tree.rs | 49 +- server/server/src/lib.rs | 5 + server/server/src/server.rs | 880 ++++++++++++++++ server/sourcefile/Cargo.toml | 23 + server/sourcefile/src/lib.rs | 75 ++ server/{main => sourcefile}/src/linemap.rs | 6 +- server/sourcefile/src/source_file.rs | 194 ++++ .../{main => sourcefile}/src/source_mapper.rs | 28 +- server/{main => }/testdata/01/common.glsl | 0 server/{main => }/testdata/01/final.fsh | 0 server/testdata/01/final.fsh.merge | 25 + server/testdata/01/shaders.properties | 0 server/{main => }/testdata/02/final.fsh | 0 server/testdata/02/final.fsh.merge | 41 + server/testdata/02/shaders.properties | 0 .../{main => }/testdata/02/utils/burger.glsl | 0 .../{main => }/testdata/02/utils/sample.glsl | 0 server/{main => }/testdata/02/utils/test.glsl | 0 server/{main => }/testdata/03/final.fsh | 0 server/testdata/03/final.fsh.merge | 37 + server/testdata/03/shaders.properties | 0 .../{main => }/testdata/03/utils/burger.glsl | 0 .../{main => }/testdata/03/utils/sample.glsl | 0 server/{main => }/testdata/03/utils/test.glsl | 0 server/{main => }/testdata/04/final.fsh | 2 +- server/testdata/04/final.fsh.merge | 37 + .../{main => }/testdata/04/lib/matrices.glsl | 0 server/testdata/04/shaders.properties | 0 .../{main => }/testdata/04/utils/stuff1.glsl | 0 .../{main => }/testdata/04/utils/stuff2.glsl | 0 .../testdata/04/utils/utilities.glsl | 0 server/{main => }/testdata/05/common.glsl | 0 server/{main => }/testdata/05/final.fsh | 0 server/{main => }/testdata/05/final.fsh.merge | 0 server/testdata/05/shaders.properties | 0 .../{main => }/testdata/05/test/banana.glsl | 0 .../{main => }/testdata/05/test/burger.glsl | 0 server/{main => }/testdata/06/final.fsh | 0 server/testdata/06/final.fsh.merge | 31 + server/testdata/06/shaders.properties | 0 server/{main => }/testdata/06/test.glsl | 0 server/workspace/Cargo.toml | 29 + server/workspace/src/lib.rs | 6 + server/workspace/src/workspace.rs | 185 ++++ server/workspace/src/workspace_manager.rs | 137 +++ server/workspace_tree/Cargo.toml | 27 + server/workspace_tree/src/lib.rs | 280 ++++++ server/workspace_tree/src/tree.rs | 11 + 105 files changed, 5156 insertions(+), 3268 deletions(-) delete mode 100644 logo-min.png create mode 100644 server/filesystem/Cargo.toml create mode 100644 server/filesystem/src/lib.rs create mode 100644 server/filesystem/src/source_norm.rs create mode 100644 server/filesystem/src/top_levels.rs create mode 100644 server/filesystem/src/url_norm.rs create mode 100644 server/graph/Cargo.toml create mode 100644 server/graph/src/dfs.rs create mode 100644 server/graph/src/graph.rs create mode 100644 server/graph/src/lib.rs create mode 100644 server/include_merger/Cargo.toml create mode 100644 server/include_merger/src/consts.rs create mode 100644 server/include_merger/src/lib.rs create mode 100644 server/include_merger/src/merge_views.rs delete mode 100644 server/main/src/commands/graph_dot.rs delete mode 100644 server/main/src/commands/merged_includes.rs delete mode 100644 server/main/src/commands/mod.rs delete mode 100644 server/main/src/consts.rs delete mode 100644 server/main/src/dfs.rs delete mode 100644 server/main/src/graph.rs delete mode 100644 server/main/src/lsp_ext.rs delete mode 100644 server/main/src/merge_views.rs delete mode 100644 server/main/src/test.rs delete mode 100644 server/main/src/url_norm.rs delete mode 100644 server/main/testdata/01/final.fsh.merge delete mode 100644 server/main/testdata/02/final.fsh.merge delete mode 100644 server/main/testdata/03/final.fsh.merge delete mode 100644 server/main/testdata/04/final.fsh.merge delete mode 100644 server/main/testdata/06/final.fsh.merge create mode 100644 server/opengl/Cargo.toml rename server/{main => opengl}/src/diagnostics_parser.rs (54%) create mode 100644 server/opengl/src/lib.rs rename server/{main/src/opengl.rs => opengl/src/opengl_context.rs} (86%) create mode 100644 server/opengl/src/opengl_context_facade.rs create mode 100644 server/server/Cargo.toml create mode 100644 server/server/src/commands/graph_dot.rs create mode 100644 server/server/src/commands/merged_includes.rs create mode 100644 server/server/src/commands/mod.rs rename server/{main => server}/src/commands/parse_tree.rs (57%) create mode 100644 server/server/src/lib.rs create mode 100644 server/server/src/server.rs create mode 100644 server/sourcefile/Cargo.toml create mode 100644 server/sourcefile/src/lib.rs rename server/{main => sourcefile}/src/linemap.rs (94%) create mode 100644 server/sourcefile/src/source_file.rs rename server/{main => sourcefile}/src/source_mapper.rs (60%) rename server/{main => }/testdata/01/common.glsl (100%) rename server/{main => }/testdata/01/final.fsh (100%) create mode 100644 server/testdata/01/final.fsh.merge create mode 100644 server/testdata/01/shaders.properties rename server/{main => }/testdata/02/final.fsh (100%) create mode 100644 server/testdata/02/final.fsh.merge create mode 100644 server/testdata/02/shaders.properties rename server/{main => }/testdata/02/utils/burger.glsl (100%) rename server/{main => }/testdata/02/utils/sample.glsl (100%) rename server/{main => }/testdata/02/utils/test.glsl (100%) rename server/{main => }/testdata/03/final.fsh (100%) create mode 100644 server/testdata/03/final.fsh.merge create mode 100644 server/testdata/03/shaders.properties rename server/{main => }/testdata/03/utils/burger.glsl (100%) rename server/{main => }/testdata/03/utils/sample.glsl (100%) rename server/{main => }/testdata/03/utils/test.glsl (100%) rename server/{main => }/testdata/04/final.fsh (66%) create mode 100644 server/testdata/04/final.fsh.merge rename server/{main => }/testdata/04/lib/matrices.glsl (100%) create mode 100644 server/testdata/04/shaders.properties rename server/{main => }/testdata/04/utils/stuff1.glsl (100%) rename server/{main => }/testdata/04/utils/stuff2.glsl (100%) rename server/{main => }/testdata/04/utils/utilities.glsl (100%) rename server/{main => }/testdata/05/common.glsl (100%) rename server/{main => }/testdata/05/final.fsh (100%) rename server/{main => }/testdata/05/final.fsh.merge (100%) create mode 100644 server/testdata/05/shaders.properties rename server/{main => }/testdata/05/test/banana.glsl (100%) rename server/{main => }/testdata/05/test/burger.glsl (100%) rename server/{main => }/testdata/06/final.fsh (100%) create mode 100644 server/testdata/06/final.fsh.merge create mode 100644 server/testdata/06/shaders.properties rename server/{main => }/testdata/06/test.glsl (100%) create mode 100644 server/workspace/Cargo.toml create mode 100644 server/workspace/src/lib.rs create mode 100644 server/workspace/src/workspace.rs create mode 100644 server/workspace/src/workspace_manager.rs create mode 100644 server/workspace_tree/Cargo.toml create mode 100644 server/workspace_tree/src/lib.rs create mode 100644 server/workspace_tree/src/tree.rs diff --git a/.vscode/launch.json b/.vscode/launch.json index 5e54c7c..7c4f667 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -4,6 +4,7 @@ "configurations": [ { "type": "extensionHost", + "trace": true, "request": "launch", "name": "Launch Client", "runtimeExecutable": "${execPath}", diff --git a/client/package-lock.json b/client/package-lock.json index d33cec9..707c680 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -10,14 +10,14 @@ "adm-zip": "^0.5.9", "encoding": "^0.1.13", "node-fetch": "^2.6.7", - "vscode-languageclient": "^6.1.4" + "vscode-languageclient": "^8.0.0" }, "devDependencies": { "@rollup/plugin-commonjs": "^21.0.2", "@rollup/plugin-node-resolve": "^13.1.3", "@types/adm-zip": "^0.4.34", "@types/node-fetch": "^2.6.1", - "@types/vscode": "^1.65.0", + "@types/vscode": "^1.70.0", "rollup": "^2.70.1" } }, @@ -134,9 +134,9 @@ } }, "node_modules/@types/vscode": { - "version": "1.65.0", - "resolved": "https://registry.npmjs.org/@types/vscode/-/vscode-1.65.0.tgz", - "integrity": "sha512-wQhExnh2nEzpjDMSKhUvnNmz3ucpd3E+R7wJkOhBNK3No6fG3VUdmVmMOKD0A8NDZDDDiQcLNxe3oGmX5SjJ5w==", + "version": "1.70.0", + "resolved": "https://registry.npmjs.org/@types/vscode/-/vscode-1.70.0.tgz", + "integrity": "sha512-3/9Fz0F2eBgwciazc94Ien+9u1elnjFg9YAhvAb3qDy/WeFWD9VrOPU7CIytryOVUdbxus8uzL4VZYONA0gDtA==", "dev": true }, "node_modules/adm-zip": { @@ -156,14 +156,12 @@ "node_modules/balanced-match": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", - "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", - "dev": true + "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=" }, "node_modules/brace-expansion": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -202,8 +200,7 @@ "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", - "dev": true + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" }, "node_modules/deepmerge": { "version": "4.2.2", @@ -362,6 +359,17 @@ "@types/estree": "*" } }, + "node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/magic-string": { "version": "0.25.7", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.25.7.tgz", @@ -396,7 +404,6 @@ "version": "3.0.4", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", - "dev": true, "dependencies": { "brace-expansion": "^1.1.7" }, @@ -491,11 +498,17 @@ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" }, "node_modules/semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "version": "7.3.7", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.7.tgz", + "integrity": "sha512-QlYTucUYOews+WeEujDoEGziz4K6c47V/Bd+LjSSYcA94p+DmINdf7ncaUinThfvZyu13lN9OY1XDxt8C0Tw0g==", + "dependencies": { + "lru-cache": "^6.0.0" + }, "bin": { "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" } }, "node_modules/sourcemap-codec": { @@ -510,38 +523,39 @@ "integrity": "sha1-gYT9NH2snNwYWZLzpmIuFLnZq2o=" }, "node_modules/vscode-jsonrpc": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/vscode-jsonrpc/-/vscode-jsonrpc-5.0.1.tgz", - "integrity": "sha512-JvONPptw3GAQGXlVV2utDcHx0BiY34FupW/kI6mZ5x06ER5DdPG/tXWMVHjTNULF5uKPOUUD0SaXg5QaubJL0A==", + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/vscode-jsonrpc/-/vscode-jsonrpc-8.0.2.tgz", + "integrity": "sha512-RY7HwI/ydoC1Wwg4gJ3y6LpU9FJRZAUnTYMXthqhFXXu77ErDd/xkREpGuk4MyYkk4a+XDWAMqe0S3KkelYQEQ==", "engines": { - "node": ">=8.0.0 || >=10.0.0" + "node": ">=14.0.0" } }, "node_modules/vscode-languageclient": { - "version": "6.1.4", - "resolved": "https://registry.npmjs.org/vscode-languageclient/-/vscode-languageclient-6.1.4.tgz", - "integrity": "sha512-EUOU+bJu6axmt0RFNo3nrglQLPXMfanbYViJee3Fbn2VuQoX0ZOI4uTYhSRvYLP2vfwTP/juV62P/mksCdTZMA==", + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/vscode-languageclient/-/vscode-languageclient-8.0.2.tgz", + "integrity": "sha512-lHlthJtphG9gibGb/y72CKqQUxwPsMXijJVpHEC2bvbFqxmkj9LwQ3aGU9dwjBLqsX1S4KjShYppLvg1UJDF/Q==", "dependencies": { - "semver": "^6.3.0", - "vscode-languageserver-protocol": "3.15.3" + "minimatch": "^3.0.4", + "semver": "^7.3.5", + "vscode-languageserver-protocol": "3.17.2" }, "engines": { - "vscode": "^1.41.0" + "vscode": "^1.67.0" } }, "node_modules/vscode-languageserver-protocol": { - "version": "3.15.3", - "resolved": "https://registry.npmjs.org/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.15.3.tgz", - "integrity": "sha512-zrMuwHOAQRhjDSnflWdJG+O2ztMWss8GqUUB8dXLR/FPenwkiBNkMIJJYfSN6sgskvsF0rHAoBowNQfbyZnnvw==", + "version": "3.17.2", + "resolved": "https://registry.npmjs.org/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.17.2.tgz", + "integrity": "sha512-8kYisQ3z/SQ2kyjlNeQxbkkTNmVFoQCqkmGrzLH6A9ecPlgTbp3wDTnUNqaUxYr4vlAcloxx8zwy7G5WdguYNg==", "dependencies": { - "vscode-jsonrpc": "^5.0.1", - "vscode-languageserver-types": "3.15.1" + "vscode-jsonrpc": "8.0.2", + "vscode-languageserver-types": "3.17.2" } }, "node_modules/vscode-languageserver-types": { - "version": "3.15.1", - "resolved": "https://registry.npmjs.org/vscode-languageserver-types/-/vscode-languageserver-types-3.15.1.tgz", - "integrity": "sha512-+a9MPUQrNGRrGU630OGbYVQ+11iOIovjCkqxajPa9w57Sd5ruK8WQNsslzpa0x/QJqC8kRc2DUxWjIFwoNm4ZQ==" + "version": "3.17.2", + "resolved": "https://registry.npmjs.org/vscode-languageserver-types/-/vscode-languageserver-types-3.17.2.tgz", + "integrity": "sha512-zHhCWatviizPIq9B7Vh9uvrH6x3sK8itC84HkamnBWoDFJtzBf7SWlpLCZUit72b3os45h6RWQNC9xHRDF8dRA==" }, "node_modules/webidl-conversions": { "version": "3.0.1", @@ -562,6 +576,11 @@ "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", "dev": true + }, + "node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" } }, "dependencies": { @@ -659,9 +678,9 @@ } }, "@types/vscode": { - "version": "1.65.0", - "resolved": "https://registry.npmjs.org/@types/vscode/-/vscode-1.65.0.tgz", - "integrity": "sha512-wQhExnh2nEzpjDMSKhUvnNmz3ucpd3E+R7wJkOhBNK3No6fG3VUdmVmMOKD0A8NDZDDDiQcLNxe3oGmX5SjJ5w==", + "version": "1.70.0", + "resolved": "https://registry.npmjs.org/@types/vscode/-/vscode-1.70.0.tgz", + "integrity": "sha512-3/9Fz0F2eBgwciazc94Ien+9u1elnjFg9YAhvAb3qDy/WeFWD9VrOPU7CIytryOVUdbxus8uzL4VZYONA0gDtA==", "dev": true }, "adm-zip": { @@ -678,14 +697,12 @@ "balanced-match": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", - "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", - "dev": true + "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=" }, "brace-expansion": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, "requires": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -715,8 +732,7 @@ "concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", - "dev": true + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" }, "deepmerge": { "version": "4.2.2", @@ -844,6 +860,14 @@ "@types/estree": "*" } }, + "lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "requires": { + "yallist": "^4.0.0" + } + }, "magic-string": { "version": "0.25.7", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.25.7.tgz", @@ -872,7 +896,6 @@ "version": "3.0.4", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", - "dev": true, "requires": { "brace-expansion": "^1.1.7" } @@ -935,9 +958,12 @@ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" }, "semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==" + "version": "7.3.7", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.7.tgz", + "integrity": "sha512-QlYTucUYOews+WeEujDoEGziz4K6c47V/Bd+LjSSYcA94p+DmINdf7ncaUinThfvZyu13lN9OY1XDxt8C0Tw0g==", + "requires": { + "lru-cache": "^6.0.0" + } }, "sourcemap-codec": { "version": "1.4.8", @@ -951,32 +977,33 @@ "integrity": "sha1-gYT9NH2snNwYWZLzpmIuFLnZq2o=" }, "vscode-jsonrpc": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/vscode-jsonrpc/-/vscode-jsonrpc-5.0.1.tgz", - "integrity": "sha512-JvONPptw3GAQGXlVV2utDcHx0BiY34FupW/kI6mZ5x06ER5DdPG/tXWMVHjTNULF5uKPOUUD0SaXg5QaubJL0A==" + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/vscode-jsonrpc/-/vscode-jsonrpc-8.0.2.tgz", + "integrity": "sha512-RY7HwI/ydoC1Wwg4gJ3y6LpU9FJRZAUnTYMXthqhFXXu77ErDd/xkREpGuk4MyYkk4a+XDWAMqe0S3KkelYQEQ==" }, "vscode-languageclient": { - "version": "6.1.4", - "resolved": "https://registry.npmjs.org/vscode-languageclient/-/vscode-languageclient-6.1.4.tgz", - "integrity": "sha512-EUOU+bJu6axmt0RFNo3nrglQLPXMfanbYViJee3Fbn2VuQoX0ZOI4uTYhSRvYLP2vfwTP/juV62P/mksCdTZMA==", + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/vscode-languageclient/-/vscode-languageclient-8.0.2.tgz", + "integrity": "sha512-lHlthJtphG9gibGb/y72CKqQUxwPsMXijJVpHEC2bvbFqxmkj9LwQ3aGU9dwjBLqsX1S4KjShYppLvg1UJDF/Q==", "requires": { - "semver": "^6.3.0", - "vscode-languageserver-protocol": "3.15.3" + "minimatch": "^3.0.4", + "semver": "^7.3.5", + "vscode-languageserver-protocol": "3.17.2" } }, "vscode-languageserver-protocol": { - "version": "3.15.3", - "resolved": "https://registry.npmjs.org/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.15.3.tgz", - "integrity": "sha512-zrMuwHOAQRhjDSnflWdJG+O2ztMWss8GqUUB8dXLR/FPenwkiBNkMIJJYfSN6sgskvsF0rHAoBowNQfbyZnnvw==", + "version": "3.17.2", + "resolved": "https://registry.npmjs.org/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.17.2.tgz", + "integrity": "sha512-8kYisQ3z/SQ2kyjlNeQxbkkTNmVFoQCqkmGrzLH6A9ecPlgTbp3wDTnUNqaUxYr4vlAcloxx8zwy7G5WdguYNg==", "requires": { - "vscode-jsonrpc": "^5.0.1", - "vscode-languageserver-types": "3.15.1" + "vscode-jsonrpc": "8.0.2", + "vscode-languageserver-types": "3.17.2" } }, "vscode-languageserver-types": { - "version": "3.15.1", - "resolved": "https://registry.npmjs.org/vscode-languageserver-types/-/vscode-languageserver-types-3.15.1.tgz", - "integrity": "sha512-+a9MPUQrNGRrGU630OGbYVQ+11iOIovjCkqxajPa9w57Sd5ruK8WQNsslzpa0x/QJqC8kRc2DUxWjIFwoNm4ZQ==" + "version": "3.17.2", + "resolved": "https://registry.npmjs.org/vscode-languageserver-types/-/vscode-languageserver-types-3.17.2.tgz", + "integrity": "sha512-zHhCWatviizPIq9B7Vh9uvrH6x3sK8itC84HkamnBWoDFJtzBf7SWlpLCZUit72b3os45h6RWQNC9xHRDF8dRA==" }, "webidl-conversions": { "version": "3.0.1", @@ -997,6 +1024,11 @@ "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", "dev": true + }, + "yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" } } } diff --git a/client/package.json b/client/package.json index 7e508dd..2668668 100644 --- a/client/package.json +++ b/client/package.json @@ -9,14 +9,14 @@ "adm-zip": "^0.5.9", "encoding": "^0.1.13", "node-fetch": "^2.6.7", - "vscode-languageclient": "^6.1.4" + "vscode-languageclient": "^8.0.0" }, "devDependencies": { "@rollup/plugin-commonjs": "^21.0.2", "@rollup/plugin-node-resolve": "^13.1.3", "@types/adm-zip": "^0.4.34", "@types/node-fetch": "^2.6.1", - "@types/vscode": "^1.65.0", + "@types/vscode": "^1.70.0", "rollup": "^2.70.1" } } diff --git a/client/src/commands.ts b/client/src/commands.ts index bb81105..0dc19d2 100644 --- a/client/src/commands.ts +++ b/client/src/commands.ts @@ -1,6 +1,6 @@ import path = require('path') import * as vscode from 'vscode' -import * as lsp from 'vscode-languageclient' +import * as lsp from 'vscode-languageclient/node' import { Extension } from './extension' import { log } from './log' @@ -10,7 +10,7 @@ export function generateGraphDot(e: Extension): Command { return async () => { await e.lspClient.sendRequest(lsp.ExecuteCommandRequest.type.method, { command: 'graphDot', - arguments: [vscode.workspace.workspaceFolders[0].uri.path], + arguments: [vscode.window.activeTextEditor.document.uri.path], }) } } @@ -26,6 +26,7 @@ export function restartExtension(e: Extension): Command { export function virtualMergedDocument(e: Extension): Command { const getVirtualDocument = async (path: string): Promise => { let content: string = '' + log.info(path) try { content = await e.lspClient.sendRequest(lsp.ExecuteCommandRequest.type.method, { command: 'virtualMerge', diff --git a/client/src/extension.ts b/client/src/extension.ts index b1863db..9ecf923 100644 --- a/client/src/extension.ts +++ b/client/src/extension.ts @@ -1,6 +1,6 @@ import { mkdirSync, promises as fs } from 'fs' import * as vscode from 'vscode' -import * as lsp from 'vscode-languageclient' +import * as lsp from 'vscode-languageclient/node' import * as commands from './commands' import { log } from './log' import { LanguageClient } from './lspClient' diff --git a/client/src/log.ts b/client/src/log.ts index 880ed52..3d047c7 100644 --- a/client/src/log.ts +++ b/client/src/log.ts @@ -1,11 +1,12 @@ import { inspect } from 'util' import * as vscode from 'vscode' -export const lspOutputChannel = vscode.window.createOutputChannel('Minecraft Shaders Language Server') +export const lspOutputChannel = vscode.window.createOutputChannel('Minecraft Shaders LSP - Server') +export const traceOutputChannel = vscode.window.createOutputChannel('Minecraft Shaders LSP - Trace') // from rust-analyzer https://github.com/rust-analyzer/rust-analyzer/blob/ef223b9e6439c228e0be49861efd2067c0b22af4/editors/code/src/util.ts export const log = new class { - readonly output = vscode.window.createOutputChannel('Minecraft Shaders'); + readonly output = vscode.window.createOutputChannel('Minecraft Shaders LSP - Client'); // Hint: the type [T, ...T[]] means a non-empty array debug(...msg: [unknown, ...unknown[]]): void { diff --git a/client/src/lspClient.ts b/client/src/lspClient.ts index 7d9d2e7..6df5f14 100644 --- a/client/src/lspClient.ts +++ b/client/src/lspClient.ts @@ -1,18 +1,35 @@ +import { ChildProcess, spawn } from 'child_process' import { ConfigurationTarget, workspace } from 'vscode' -import * as lsp from 'vscode-languageclient' +import * as lsp from 'vscode-languageclient/node' +import { PublishDiagnosticsNotification, StreamInfo, TelemetryEventNotification } from 'vscode-languageclient/node' import { Extension } from './extension' -import { log, lspOutputChannel } from './log' -import { ConfigUpdateParams, statusMethod, StatusParams, updateConfigMethod } from './lspExt' +import { log, lspOutputChannel, traceOutputChannel } from './log' +import { statusMethod, StatusParams } from './lspExt' export class LanguageClient extends lsp.LanguageClient { private extension: Extension constructor(ext: Extension, lspBinary: string, filewatcherGlob: string) { - super('vscode-mc-shader', 'VSCode MC Shader', { - command: lspBinary - }, { + const serverOptions = () => new Promise((resolve, reject) => { + const childProcess = spawn(lspBinary, { + env: { + 'RUST_BACKTRACE': 'true', + ...process.env, + } + }) + childProcess.stderr.on('data', (data: Buffer) => { + lspOutputChannel.appendLine(data.toString().trimRight()) + }) + childProcess.on('exit', (code, signal) => { + lspOutputChannel.appendLine(`⚠️⚠️⚠️ Language server exited ` + (signal ? `from signal ${signal}` : `with exit code ${code}`) + ' ⚠️⚠️⚠️') + }) + resolve(childProcess) + }) + + super('mcglsl', '', serverOptions, { + traceOutputChannel: traceOutputChannel, + diagnosticCollectionName: 'mcglsl', documentSelector: [{ scheme: 'file', language: 'glsl' }], - outputChannel: lspOutputChannel, synchronize: { configurationSection: 'mcglsl', fileEvents: workspace.createFileSystemWatcher(filewatcherGlob) @@ -25,17 +42,27 @@ export class LanguageClient extends lsp.LanguageClient { } public startServer = async (): Promise => { - this.extension.context.subscriptions.push(this.start()) + // this.extension.context.subscriptions.push(this.start()) + this.setTrace(lsp.Trace.Verbose) - await this.onReady() + this.extension.context.subscriptions.push(this.onNotification(PublishDiagnosticsNotification.type, (p) => { + log.error(JSON.stringify(p)) + })) + this.extension.context.subscriptions.push(this.onNotification(TelemetryEventNotification.type, this.onStatusChange)) - this.onNotification(updateConfigMethod, this.onUpdateConfig) - this.onNotification(statusMethod, this.onStatusChange) + await this.start() + // await this.onReady() + console.log('banana') return this } - onStatusChange = (params: StatusParams) => { + onStatusChange = (params: { + status: 'loading' | 'ready' | 'failed' | 'clear' + message: string + icon: string + }) => { + log.info('bananan') switch (params.status) { case 'loading': case 'ready': @@ -47,10 +74,4 @@ export class LanguageClient extends lsp.LanguageClient { break } } - - onUpdateConfig = (params: ConfigUpdateParams) => { - for (const kv of params.kv) { - workspace.getConfiguration().update('mcglsl.' + kv.key, kv.value, ConfigurationTarget.Global) - } - } } diff --git a/client/src/lspExt.ts b/client/src/lspExt.ts index 70b40da..3508ced 100644 --- a/client/src/lspExt.ts +++ b/client/src/lspExt.ts @@ -1,4 +1,4 @@ -import * as lsp from 'vscode-languageclient' +import * as lsp from 'vscode-languageclient/node' export type StatusParams = { status: 'loading' | 'ready' | 'failed' | 'clear' @@ -6,11 +6,10 @@ export type StatusParams = { icon: string } -export const statusMethod = 'mc-glsl/status' -export const status = new lsp.NotificationType(statusMethod) +export const statusMethod = new lsp.NotificationType('mc-glsl/status') -export const updateConfigMethod = 'mc-glsl/updateConfig' +/* export const updateConfigMethod = 'mc-glsl/updateConfig' export type ConfigUpdateParams = { kv: { key: string, value: string }[] -} \ No newline at end of file +} */ \ No newline at end of file diff --git a/logo-min.png b/logo-min.png deleted file mode 100644 index 4faf57b43c56bcc9405a43d8b37f44ea7f472801..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 25081 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 diff --git a/logo-mini.png b/logo-mini.png index 948748b3cf3a3db27e0eaa7d9e6f921e708498e1..4faf57b43c56bcc9405a43d8b37f44ea7f472801 100644 GIT binary patch delta 21705 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"] +edition = "2021" + +[lib] +doctest = false + +[dependencies] +path-slash = "0.1" +anyhow = "1.0" +url = "2.2" +percent-encoding = "2.1" +serde_json = "1.0" +logging = { path = "../logging" } +tokio = { version = "1.18", features = ["fs"]} +regex = "1.4" +lazy_static = "1.4" diff --git a/server/filesystem/src/lib.rs b/server/filesystem/src/lib.rs new file mode 100644 index 0000000..c39170c --- /dev/null +++ b/server/filesystem/src/lib.rs @@ -0,0 +1,6 @@ +mod url_norm; +mod source_norm; +mod top_levels; +pub use url_norm::*; +pub use source_norm::*; +pub use top_levels::*; \ No newline at end of file diff --git a/server/filesystem/src/source_norm.rs b/server/filesystem/src/source_norm.rs new file mode 100644 index 0000000..8c1d3d3 --- /dev/null +++ b/server/filesystem/src/source_norm.rs @@ -0,0 +1,41 @@ +use anyhow::{anyhow, Result}; +#[cfg(not(test))] +use std::fs; +use std::{ops::Deref, path::Path}; +#[cfg(test)] +use { + logging::{logger, FutureExt}, + tokio::fs, +}; + +/// Denotes a string with any `\r\n` replaced with `\n` +#[derive(Clone, Debug, Hash, PartialEq, Eq, PartialOrd, Ord)] +pub struct LFString(String); + +impl LFString { + pub async fn read>(path: P) -> Result { + #[cfg(test)] + let read_result = fs::read_to_string(&path).with_logger(logger()).await; + #[cfg(not(test))] + let read_result = fs::read_to_string(&path); + + let source = read_result.map_err(|e| anyhow!("error reading {:?}: {}", path.as_ref(), e))?; + Ok(LFString(source.replace("\r\n", "\n"))) + } + + pub fn with_capacity(capacity: usize) -> Self { + LFString(String::with_capacity(capacity)) + } + + pub fn from_unchecked(string: String) -> Self { + LFString(string) + } +} + +impl Deref for LFString { + type Target = str; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} diff --git a/server/filesystem/src/top_levels.rs b/server/filesystem/src/top_levels.rs new file mode 100644 index 0000000..806939f --- /dev/null +++ b/server/filesystem/src/top_levels.rs @@ -0,0 +1,65 @@ +use std::collections::HashSet; + +use lazy_static::lazy_static; +use regex::Regex; + +use crate::NormalizedPathBuf; + +lazy_static! { + static ref RE_WORLD_FOLDER: Regex = Regex::new(r#"^shaders(/world-?\d+)?"#).unwrap(); + static ref TOPLEVEL_FILES: HashSet = { + 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)); + } + set + }; +} + +pub fn is_top_level(path: &NormalizedPathBuf) -> bool { + let path_str = &path.to_string(); + if !RE_WORLD_FOLDER.is_match(path_str) { + return false; + } + let parts: Vec<&str> = path_str.split('/').collect(); + let len = parts.len(); + (len == 3 || len == 2) && TOPLEVEL_FILES.contains(parts[len - 1]) +} \ No newline at end of file diff --git a/server/filesystem/src/url_norm.rs b/server/filesystem/src/url_norm.rs new file mode 100644 index 0000000..8aec4db --- /dev/null +++ b/server/filesystem/src/url_norm.rs @@ -0,0 +1,151 @@ +use std::{ + fmt::{Display, Debug}, + path::{Path, PathBuf}, +}; + +use anyhow::Result; +use logging::trace; +use path_slash::PathBufExt; +use serde_json::value::Value; +use url::Url; + +#[derive(Clone, Hash, PartialEq, Eq, PartialOrd, Ord)] +pub struct NormalizedPathBuf(PathBuf); + +impl NormalizedPathBuf { + pub fn join(&self, path: impl Into) -> NormalizedPathBuf { + NormalizedPathBuf(PathBuf::from_slash(self.0.join(path.into()).to_str().unwrap())) + } + + pub fn parent(&self) -> Option { + self.0.parent().map(Into::into) + } + + pub fn extension(&self) -> Option<&str> { + self.0.extension().and_then(|e| e.to_str()) + } + + pub fn strip_prefix(&self, prefix: &Self) -> NormalizedPathBuf { + self.0.strip_prefix(prefix.clone().0).unwrap().into() + } + + pub fn exists(&self) -> bool { + self.0.exists() + } +} + +impl Debug for NormalizedPathBuf { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_fmt(format_args!("{:?}", self.0)) + } +} + +impl AsRef for NormalizedPathBuf { + fn as_ref(&self) -> &Path { + self.0.as_path() + } +} + +impl AsRef for NormalizedPathBuf { + fn as_ref(&self) -> &PathBuf { + &self.0 + } +} + +impl From<&NormalizedPathBuf> for PathBuf { + fn from(p: &NormalizedPathBuf) -> Self { + PathBuf::from_slash(p.0.to_str().unwrap()) + } +} + +impl From<&Path> for NormalizedPathBuf { + fn from(p: &Path) -> Self { + // TODO: is this right?? + PathBuf::from_slash(p.to_str().unwrap()).into() + } +} + +impl From for NormalizedPathBuf { + fn from(p: PathBuf) -> Self { + // don't use p.as_path().into(), it'll cause infinite recursion with above impl + p.to_str().unwrap().into() + } +} + +impl From<&str> for NormalizedPathBuf { + fn from(s: &str) -> Self { + // TODO: is this right?? + NormalizedPathBuf(PathBuf::from_slash(s)) + } +} + + +impl logging::Value for NormalizedPathBuf { + fn serialize(&self, record: &logging::Record, key: logging::Key, serializer: &mut dyn logging::Serializer) -> logging::Result { + self.0.to_str().unwrap().serialize(record, key, serializer) + } +} + +impl Display for NormalizedPathBuf { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.0.display()) + } +} + +impl From for NormalizedPathBuf { + #[cfg(target_family = "windows")] + fn from(u: Url) -> Self { + let path = PathBuf::from_slash( + 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_str().unwrap()); + + NormalizedPathBuf(path) + } + + #[cfg(target_family = "unix")] + fn from(u: Url) -> Self { + let path = PathBuf::from_slash(percent_encoding::percent_decode_str(u.path()).decode_utf8().unwrap()); + + trace!("converted unix path from url"; "old" => u.as_str(), "new" => path.to_str().unwrap()); + + NormalizedPathBuf(path) + } +} + +impl TryFrom<&Value> for NormalizedPathBuf { + type Error = anyhow::Error; + + #[cfg(target_family = "windows")] + fn try_from(v: &Value) -> Result { + if !v.is_string() { + return Err(anyhow::format_err!("cannot convert {:?} to PathBuf", v)); + } + let path = v.to_string(); + let path = PathBuf::from_slash( + percent_encoding::percent_decode_str(path.trim_start_matches('"').trim_end_matches('"').strip_prefix('/').unwrap()) + .decode_utf8()?, + ); + + trace!("converted win path from json"; "old" => v.to_string(), "new" => path.to_str().unwrap()); + + Ok(NormalizedPathBuf(path)) + } + + #[cfg(target_family = "unix")] + fn try_from(v: &serde_json::value::Value) -> Result { + if !v.is_string() { + return Err(anyhow::format_err!("cannot convert {:?} to PathBuf", v)); + } + let path = v.to_string(); + let path = + PathBuf::from_slash(percent_encoding::percent_decode_str(path.trim_start_matches('"').trim_end_matches('"')).decode_utf8()?); + + trace!("converted unix path from json"; "old" => v.to_string(), "new" => path.to_str().unwrap()); + + Ok(NormalizedPathBuf(path)) + } +} diff --git a/server/graph/Cargo.toml b/server/graph/Cargo.toml new file mode 100644 index 0000000..236c891 --- /dev/null +++ b/server/graph/Cargo.toml @@ -0,0 +1,25 @@ +[package] +name = "graph" +version = "0.9.8" +authors = ["Noah Santschi-Cooney "] +edition = "2021" + +[lib] +doctest = false + +[dependencies] +anyhow = "1.0" +petgraph = "0.6" +logging = { path = "../logging" } +logging_macro = { path = "../logging_macro" } +slog = { version = "2.7", features = [ "max_level_trace", "release_max_level_trace" ] } +slog-scope = "4.4" +sourcefile = { path = "../sourcefile" } +tower-lsp = "0.17.0" +thiserror = "1.0" + +[dev-dependencies] +tempdir = "0.3" +fs_extra = "1.2" +hamcrest2 = "*" +pretty_assertions = "1.1" \ No newline at end of file diff --git a/server/graph/src/dfs.rs b/server/graph/src/dfs.rs new file mode 100644 index 0000000..fd21253 --- /dev/null +++ b/server/graph/src/dfs.rs @@ -0,0 +1,342 @@ +use crate::{graph::CachedStableGraph, FilialTuple}; +use petgraph::stable_graph::{NodeIndex, StableDiGraph}; +use std::{fmt::Debug, hash::Hash}; + +use anyhow::Result; + +struct VisitCount { + node: NodeIndex, + // how many times we have backtracked to this node + // after exhausting a DFS along one of this node's + // outgoing edges + touch: usize, + // how many times we have to backtrack to this node + // after exhausting a DFS along one of this node's + // outgoing edges before we backtrack to the parent + // node of this node that we came from during the + // traversal. Aint that a mouthful. + children: usize, +} + +/// Performs a depth-first search with duplicates +pub struct Dfs<'a, K, V> +where + K: Hash + Clone + ToString + Eq + Debug, + V: Ord + Copy, +{ + graph: &'a CachedStableGraph, + // TODO: how can we collapse these + stack: Vec, + cycle: Vec, +} + +impl<'a, K, V> Dfs<'a, K, V> +where + K: Hash + Clone + ToString + Eq + Debug, + V: Ord + Copy, +{ + 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<(), CycleError> { + for prev in &self.cycle { + for child in children { + if prev.node == *child { + let cycle_nodes: Vec = self.cycle.iter().map(|n| n.node).collect(); + return Err(CycleError::new(&cycle_nodes, *child, self.graph)); + } + } + } + Ok(()) + } +} + +impl<'a, K, V> Iterator for Dfs<'a, K, V> +where + K: Hash + Clone + ToString + Eq + Debug, + V: Ord + Copy, +{ + type Item = Result, CycleError>; + + fn next(&mut self) -> Option, CycleError>> { + let parent = self.cycle.last().map(|p| p.node); + + if let Some(child) = self.stack.pop() { + self.cycle.push(VisitCount { + node: child, + children: Into::<&StableDiGraph>::into(self.graph).edges(child).count(), + touch: 1, + }); + + let children: Vec<_> = self.graph.get_all_children(child).rev().collect(); + + if !children.is_empty() { + if let Err(e) = self.check_for_cycle(&children) { + return Some(Err(e)); + } + + for child in children { + self.stack.push(child); + } + } else { + self.reset_path_to_branch(); + } + + return Some(Ok(FilialTuple { child, parent })); + } + None + } +} + +use tower_lsp::lsp_types::{Diagnostic, DiagnosticSeverity, Position, Range}; + +use std::{error::Error as StdError, fmt::Display}; + +#[derive(Debug)] +pub struct CycleError(Vec); + +impl StdError for CycleError {} + +impl CycleError { + pub fn new(nodes: &[NodeIndex], current_node: NodeIndex, graph: &CachedStableGraph) -> Self + where + K: Hash + Clone + ToString + Eq + Debug, + V: Ord + Copy, + { + let mut resolved_nodes: Vec = nodes.iter().map(|i| graph[*i].clone()).collect(); + resolved_nodes.push(graph[current_node].clone()); + CycleError(resolved_nodes.into_iter().map(|p| p.to_string()).collect()) + } +} + +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 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("mcglsl".into()), + message: e.into(), + code: None, + tags: None, + related_information: None, + code_description: Option::None, + data: Option::None, + } + } +} + +impl From for String { + fn from(e: CycleError) -> String { + format!("{}", e) + } +} + +#[cfg(test)] +mod dfs_test { + use hamcrest2::prelude::*; + use hamcrest2::{assert_that, ok}; + use petgraph::stable_graph::StableDiGraph; + use petgraph::{algo::is_cyclic_directed, graph::NodeIndex}; + + use crate::dfs; + use crate::graph::CachedStableGraph; + + #[test] + #[logging_macro::scope] + fn test_graph_dfs() { + { + let mut graph = CachedStableGraph::new(); + + let idx0 = graph.add_node(&"0".to_string()); + let idx1 = graph.add_node(&"1".to_string()); + let idx2 = graph.add_node(&"2".to_string()); + let idx3 = graph.add_node(&"3".to_string()); + + graph.add_edge(idx0, idx1, 2); + graph.add_edge(idx0, idx2, 3); + graph.add_edge(idx1, idx3, 5); + + 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 = collection.iter().map(|n| n.child).collect(); + let parents: Vec> = 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(Into::<&StableDiGraph<_, _>>::into(&graph))); + } + { + let mut graph = CachedStableGraph::new(); + + let idx0 = graph.add_node(&"0".to_string()); + let idx1 = graph.add_node(&"1".to_string()); + let idx2 = graph.add_node(&"2".to_string()); + let idx3 = graph.add_node(&"3".to_string()); + let idx4 = graph.add_node(&"4".to_string()); + let idx5 = graph.add_node(&"5".to_string()); + let idx6 = graph.add_node(&"6".to_string()); + let idx7 = graph.add_node(&"7".to_string()); + + graph.add_edge(idx0, idx1, 2); + graph.add_edge(idx0, idx2, 3); + graph.add_edge(idx1, idx3, 5); + graph.add_edge(idx1, idx4, 6); + graph.add_edge(idx2, idx4, 5); + graph.add_edge(idx2, idx5, 4); + graph.add_edge(idx3, idx6, 4); + graph.add_edge(idx4, idx6, 4); + graph.add_edge(idx6, idx7, 4); + + 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 = collection.iter().map(|n| n.child).collect(); + let parents: Vec> = 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(Into::<&StableDiGraph<_, _>>::into(&graph))); + } + } + + #[test] + #[logging_macro::scope] + fn test_graph_dfs_cycle() { + { + let mut graph = CachedStableGraph::new(); + + let idx0 = graph.add_node(&"0".to_string()); + let idx1 = graph.add_node(&"1".to_string()); + let idx2 = graph.add_node(&"2".to_string()); + let idx3 = graph.add_node(&"3".to_string()); + let idx4 = graph.add_node(&"4".to_string()); + let idx5 = graph.add_node(&"5".to_string()); + let idx6 = graph.add_node(&"6".to_string()); + let idx7 = graph.add_node(&"7".to_string()); + + graph.add_edge(idx0, idx1, 2); + graph.add_edge(idx0, idx2, 3); + graph.add_edge(idx1, idx3, 5); + graph.add_edge(idx1, idx4, 6); + graph.add_edge(idx2, idx4, 5); + graph.add_edge(idx2, idx5, 4); + graph.add_edge(idx3, idx6, 4); + graph.add_edge(idx4, idx6, 4); + graph.add_edge(idx6, idx7, 4); + graph.add_edge(idx7, idx4, 4); + + 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(Into::<&StableDiGraph<_, _>>::into(&graph))); + } + { + let mut graph = CachedStableGraph::new(); + + let idx0 = graph.add_node(&"0".to_string()); + let idx1 = graph.add_node(&"1".to_string()); + + graph.add_edge(idx0, idx1, 2); + graph.add_edge(idx1, idx0, 2); + + let mut dfs = dfs::Dfs::new(&graph, idx1); + + println!("{:?}", dfs.next()); + println!("{:?}", dfs.next()); + println!("{:?}", dfs.next()); + } + } +} diff --git a/server/graph/src/graph.rs b/server/graph/src/graph.rs new file mode 100644 index 0000000..b6ad744 --- /dev/null +++ b/server/graph/src/graph.rs @@ -0,0 +1,454 @@ +use anyhow::format_err; +use anyhow::Result; +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}; +use std::fmt::Debug; +use std::hash::Hash; +use std::ops::Index; +use std::ops::IndexMut; + +/// 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 +where + K: Hash + Clone + Eq + Debug, + V: Ord + Copy, +{ + // StableDiGraph is used as it allows for String node values, essential for + // generating the GraphViz DOT render. + pub graph: StableDiGraph, + cache: HashMap, + // Maps a node index to its abstracted string representation. + // Mainly used as the graph is based on NodeIndex. + #[cfg(test)] + reverse_index: HashMap, +} + +impl CachedStableGraph +where + K: Hash + Clone + Eq + Debug, + V: Ord + Copy, +{ + #[allow(clippy::new_without_default)] + pub fn new() -> Self { + CachedStableGraph { + graph: StableDiGraph::new(), + cache: HashMap::new(), + #[cfg(test)] + reverse_index: HashMap::new(), + } + } + + #[inline] + pub fn node_count(&self) -> usize { + self.graph.node_count() + } + + // #[inline] + // pub fn inner(&self) -> &StableDiGraph { + // &self.graph + // } + + pub fn parents(&self, node: NodeIndex) -> impl Iterator + '_ { + self.graph.edges_directed(node, Direction::Incoming).map(|e| e.source()) + } + + /// 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: &K) -> Option { + 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); + if let Some(n) = n { + self.cache.insert(name.clone(), n); + } + n + } + } + } + + /// Returns all child node indexes for a parent, in order of import. May include duplicates if a child + /// is imported more than once into the parent. + pub fn get_all_children(&self, parent: NodeIndex) -> impl DoubleEndedIterator + '_ { + self.get_all_edges_from(parent).map(|p| p.0) + } + + /// Returns an iterator over all the edge values of type `V`'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_edges_between(&self, parent: NodeIndex, child: NodeIndex) -> impl DoubleEndedIterator + '_ { + 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::>(); + edges.sort(); + edges.into_iter() + } + + /// Returns an iterator over all the `(NodeIndex, T)` tuples between a node and all its children, in order + /// of import. + pub fn get_all_edges_from(&self, parent: NodeIndex) -> impl DoubleEndedIterator + '_ { + let mut edges = self + .graph + .edges(parent) + .map(|edge| { + let child = self.graph.edge_endpoints(edge.id()).unwrap().1; + (child, self.graph[edge.id()]) + }) + .collect::>(); + edges.sort_by(|x, y| x.1.cmp(&y.1)); + edges.into_iter() + } + + // pub fn symmetric_closure(&self) {} + + pub fn add_node(&mut self, name: &K) -> NodeIndex { + if let Some(idx) = self.cache.get(name) { + return *idx; + } + let idx = self.graph.add_node(name.clone()); + self.cache.insert(name.to_owned(), idx); + #[cfg(test)] + self.reverse_index.insert(idx, name.to_owned()); + idx + } + + /// Adds a directional edge of type `V` between `parent` and `child`. + #[inline] + pub fn add_edge(&mut self, parent: NodeIndex, child: NodeIndex, meta: V) -> EdgeIndex { + self.graph.add_edge(parent, child, meta) + } + + #[inline] + pub fn remove_edge(&mut self, parent: NodeIndex, child: NodeIndex, position: V) { + 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)); + } + + #[inline] + pub fn child_node_indexes(&self, node: NodeIndex) -> impl Iterator + '_ { + self.graph.neighbors(node) + } + + #[inline] + pub fn parent_node_indexes(&self, node: NodeIndex) -> impl Iterator + '_ { + self.graph.neighbors_directed(node, Direction::Incoming) + } + + pub fn root_ancestors_for_key(&mut self, path: &K) -> Result>> { + let node = match self.find_node(path) { + Some(n) => n, + None => return Err(format_err!("node not found {:?}", path)), + }; + Ok(self.root_ancestors(node)) + } + + #[inline] + pub fn root_ancestors(&self, node: NodeIndex) -> Option> { + 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) -> Option> { + if node == initial && !visited.is_empty() { + return None; + } + + let parents: Vec<_> = self.parent_node_indexes(node).collect(); + let mut collection = Vec::with_capacity(parents.len()); + + for ancestor in &parents { + visited.insert(*ancestor); + } + + for ancestor in &parents { + if self.parent_node_indexes(*ancestor).next().is_some() { + collection.extend(self.get_root_ancestors(initial, *ancestor, visited).unwrap_or_default()); + } else { + collection.push(*ancestor); + } + } + + Some(collection) + } +} + +impl Index for CachedStableGraph +where + K: Hash + Clone + Eq + Debug, + V: Ord + Copy, +{ + type Output = K; + + #[inline] + fn index(&self, index: NodeIndex) -> &Self::Output { + &self.graph[index] + } +} + +impl IndexMut for CachedStableGraph +where + K: Hash + Clone + Eq + Debug, + V: Ord + Copy, +{ + #[inline] + fn index_mut(&mut self, index: NodeIndex) -> &mut Self::Output { + self.graph.index_mut(index) + } +} + +#[cfg(test)] +impl CachedStableGraph +where + K: Hash + Clone + Eq + Debug, + V: Ord + Copy, +{ + fn parent_node_names(&self, node: NodeIndex) -> Vec { + 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 { + self.graph + .neighbors(node) + .map(|n| self.reverse_index.get(&n).unwrap().clone()) + .collect() + } + + fn remove_node(&mut self, name: &K) { + let idx = self.cache.remove(name); + if let Some(idx) = idx { + self.graph.remove_node(idx); + } + } +} + +impl<'a, K, V> From<&'a CachedStableGraph> for &'a StableDiGraph +where + K: Hash + Clone + Eq + Debug, + V: Ord + Copy, +{ + #[inline] + fn from(val: &'a CachedStableGraph) -> Self { + &val.graph + } +} + +#[cfg(test)] +mod graph_test { + use petgraph::graph::NodeIndex; + + use crate::graph::CachedStableGraph; + + #[test] + #[logging_macro::scope] + fn test_graph_two_connected_nodes() { + let mut graph = CachedStableGraph::new(); + + let idx1 = graph.add_node(&"sample"); + let idx2 = graph.add_node(&"banana"); + graph.add_edge(idx1, idx2, 100); + + let children = graph.child_node_names(idx1); + assert_eq!(children.len(), 1); + assert_eq!(children[0], "banana"); + + let children: Vec = 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], "sample"); + + let parents: Vec<_> = graph.parent_node_indexes(idx2).collect(); + assert_eq!(parents.len(), 1); + assert_eq!(parents[0], idx1); + + let ancestors = graph.root_ancestors(idx2).unwrap(); + assert_eq!(ancestors.len(), 1); + assert_eq!(ancestors[0], idx1); + + let ancestors = graph.root_ancestors(idx1).unwrap(); + assert_eq!(ancestors.len(), 0); + + graph.remove_node(&"sample"); + assert_eq!(graph.graph.node_count(), 1); + assert!(graph.find_node(&"sample").is_none()); + + let neighbors = graph.child_node_names(idx2); + assert_eq!(neighbors.len(), 0); + } + + #[test] + #[logging_macro::scope] + fn test_double_import() { + let mut graph = CachedStableGraph::new(); + + let idx0 = graph.add_node(&"0"); + let idx1 = graph.add_node(&"1"); + + graph.add_edge(idx0, idx1, 200); + graph.add_edge(idx0, idx1, 400); + + // 0 + // / \ + // 1 1 + + assert_eq!(2, graph.get_edges_between(idx0, idx1).count()); + + let mut edge_metas = graph.get_edges_between(idx0, idx1); + assert_eq!(Some(200), edge_metas.next()); + assert_eq!(Some(400), edge_metas.next()); + } + + #[test] + #[logging_macro::scope] + fn test_collect_root_ancestors() { + { + let mut graph = CachedStableGraph::new(); + + let idx0 = graph.add_node(&"0"); + let idx1 = graph.add_node(&"1"); + let idx2 = graph.add_node(&"2"); + let idx3 = graph.add_node(&"3"); + + graph.add_edge(idx0, idx1, 200); + graph.add_edge(idx1, idx2, 300); + graph.add_edge(idx3, idx1, 400); + + // 0 3 + // |/ + // 1 + // | + // 2 + + let roots = graph.root_ancestors(idx2).unwrap(); + assert_eq!(roots, vec![idx3, idx0]); + + let roots = graph.root_ancestors(idx1).unwrap(); + assert_eq!(roots, vec![idx3, idx0]); + + let roots = graph.root_ancestors(idx0).unwrap(); + assert_eq!(roots, vec![]); + + let roots = graph.root_ancestors(idx3).unwrap(); + assert_eq!(roots, vec![]); + } + { + let mut graph = CachedStableGraph::new(); + + let idx0 = graph.add_node(&"0"); + let idx1 = graph.add_node(&"1"); + let idx2 = graph.add_node(&"2"); + let idx3 = graph.add_node(&"3"); + + graph.add_edge(idx0, idx1, 200); + graph.add_edge(idx0, idx2, 300); + graph.add_edge(idx1, idx3, 500); + + // 0 + // / \ + // 1 2 + // / + // 3 + + let roots = graph.root_ancestors(idx3).unwrap(); + assert_eq!(roots, vec![idx0]); + + let roots = graph.root_ancestors(idx2).unwrap(); + assert_eq!(roots, vec![idx0]); + + let roots = graph.root_ancestors(idx1).unwrap(); + assert_eq!(roots, vec![idx0]); + + let roots = graph.root_ancestors(idx0).unwrap(); + assert_eq!(roots, vec![]); + } + { + let mut graph = CachedStableGraph::new(); + + let idx0 = graph.add_node(&"0"); + let idx1 = graph.add_node(&"1"); + let idx2 = graph.add_node(&"2"); + let idx3 = graph.add_node(&"3"); + + graph.add_edge(idx0, idx1, 200); + graph.add_edge(idx2, idx3, 300); + graph.add_edge(idx1, idx3, 500); + + // 0 + // \ + // 2 1 + // \ / + // 3 + + let roots = graph.root_ancestors(idx3).unwrap(); + assert_eq!(roots, vec![idx0, idx2]); + + let roots = graph.root_ancestors(idx2).unwrap(); + assert_eq!(roots, vec![]); + + let roots = graph.root_ancestors(idx1).unwrap(); + assert_eq!(roots, vec![idx0]); + + let roots = graph.root_ancestors(idx0).unwrap(); + assert_eq!(roots, vec![]); + } + { + let mut graph = CachedStableGraph::new(); + + let idx0 = graph.add_node(&"0"); + let idx1 = graph.add_node(&"1"); + let idx2 = graph.add_node(&"2"); + let idx3 = graph.add_node(&"3"); + + graph.add_edge(idx0, idx1, 200); + graph.add_edge(idx1, idx2, 400); + graph.add_edge(idx1, idx3, 600); + + // 0 + // | + // 1 + // / \ + // 2 3 + + let roots = graph.root_ancestors(idx3).unwrap(); + assert_eq!(roots, vec![idx0]); + + let roots = graph.root_ancestors(idx2).unwrap(); + assert_eq!(roots, vec![idx0]); + + let roots = graph.root_ancestors(idx1).unwrap(); + assert_eq!(roots, vec![idx0]); + + let roots = graph.root_ancestors(idx0).unwrap(); + assert_eq!(roots, vec![]); + } + } +} diff --git a/server/graph/src/lib.rs b/server/graph/src/lib.rs new file mode 100644 index 0000000..e83eefd --- /dev/null +++ b/server/graph/src/lib.rs @@ -0,0 +1,18 @@ +mod graph; +pub mod dfs; +pub use graph::*; + +pub use petgraph::stable_graph::NodeIndex; +pub use petgraph::dot::Config; +pub use petgraph::dot; + +/// 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)] +pub struct FilialTuple { + // pub child: NodeIndex, + // pub parent: Option, + pub child: T, + pub parent: Option, +} diff --git a/server/include_merger/Cargo.toml b/server/include_merger/Cargo.toml new file mode 100644 index 0000000..7cecd7f --- /dev/null +++ b/server/include_merger/Cargo.toml @@ -0,0 +1,29 @@ +[package] +name = "include_merger" +version = "0.9.8" +authors = ["Noah Santschi-Cooney "] +edition = "2021" + +[lib] +doctest = false + +[dependencies] +anyhow = "1.0" +tower-lsp = "0.17.0" +logging = { path = "../logging" } +logging_macro = { path = "../logging_macro" } +graph = { path = "../graph" } + +filesystem = { path = "../filesystem" } +tokio = { version = "1.18", features = ["fs"]} + +workspace_tree = { path = "../workspace_tree" } +sourcefile = { path = "../sourcefile" } +tree-sitter = "0.20.6" +tree-sitter-glsl = "0.1.2" +opengl = { path = "../opengl" } + +[dev-dependencies] +tempdir = "0.3" +fs_extra = "1.2" +pretty_assertions = "1.2" \ No newline at end of file diff --git a/server/include_merger/src/consts.rs b/server/include_merger/src/consts.rs new file mode 100644 index 0000000..e27d958 --- /dev/null +++ b/server/include_merger/src/consts.rs @@ -0,0 +1,13 @@ +pub const OPTIFINE_PREAMBLE: &str = r#"#define MC_VERSION 11800 +#define MC_GL_VERSION 320 +#define MC_GLSL_VERSION 150 +#define MC_OS_LINUX +#define MC_GL_VENDOR_NVIDIA +#define MC_GL_RENDERER_GEFORCE +#define MC_NORMAL_MAP +#define MC_SPECULAR_MAP +#define MC_RENDER_QUALITY 1.0 +#define MC_SHADOW_QUALITY 1.0 +#define MC_HAND_DEPTH 0.125 +#define MC_OLD_HAND_LIGHT +#define MC_OLD_LIGHTING"#; \ No newline at end of file diff --git a/server/include_merger/src/lib.rs b/server/include_merger/src/lib.rs new file mode 100644 index 0000000..73f85d5 --- /dev/null +++ b/server/include_merger/src/lib.rs @@ -0,0 +1,3 @@ +mod merge_views; +mod consts; +pub use merge_views::*; \ No newline at end of file diff --git a/server/include_merger/src/merge_views.rs b/server/include_merger/src/merge_views.rs new file mode 100644 index 0000000..cce9d25 --- /dev/null +++ b/server/include_merger/src/merge_views.rs @@ -0,0 +1,572 @@ +use std::cmp::min; +use std::collections::{HashMap, LinkedList, VecDeque}; +use std::iter::Peekable; + +use core::slice::Iter; + +use filesystem::{LFString, NormalizedPathBuf}; +use graph::FilialTuple; +use logging::debug; + +use crate::consts; +use sourcefile::{IncludeLine, SourceFile, SourceMapper, Version}; + +/// 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: Peekable>>, + + // sources: &'a HashMap, + /// contains additionally inserted lines such as #line and other directives, preamble defines etc + extra_lines: Vec, + + // 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, 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, Box<(dyn Iterator + 'a)>>, + + // #line directives need to be adjusted based on GPU vendor + document glsl version + gpu_vendor: opengl::GPUVendor, + document_glsl_version: sourcefile::Version, +} + +impl<'a> MergeViewBuilder<'a> { + pub fn new( + nodes: &'a [FilialTuple<&'a SourceFile>], source_mapper: &'a mut SourceMapper, gpu_vendor: opengl::GPUVendor, + document_glsl_version: sourcefile::Version, + ) -> Self { + println!("{}", nodes.len()); + MergeViewBuilder { + nodes: nodes.iter().peekable(), + extra_lines: Vec::with_capacity((nodes.len() * 2) + 2), + source_mapper, + last_offset_set: HashMap::new(), + parent_child_edge_iterator: HashMap::new(), + gpu_vendor, + document_glsl_version, + } + } + + pub fn build(&mut self) -> LFString { + // 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.next().unwrap().child; + let first_path = &first.path; + let first_source = &first.source; + + // seed source_mapper with top-level file + self.source_mapper.get_num(&first.path); + + // add the optifine preamble (and extra compatibility mangling eventually) + let version_line_offset = self.find_version_offset(first_source); + let (version_char_for_line, version_char_following_line) = self.char_offset_for_line(version_line_offset, first_source); + eprintln!( + "line {} char for line {} char after line {}", + version_line_offset, version_char_for_line, version_char_following_line + ); + self.add_preamble( + version_line_offset, + version_char_following_line, + first_path, + first_source, + &mut merge_list, + ); + + self.set_last_offset_for_tuple(None, first_path, version_char_following_line); + // self.set_last_offset_for_tuple(None, first, 0); + + // stack to keep track of the depth first traversal + let mut stack: VecDeque<_> = VecDeque::<&'a NormalizedPathBuf>::new(); + + // where the magic happens! + self.create_merge_views(&mut merge_list, &mut stack); + + // now we add a view of the remainder of the root file + let offset = self.get_last_offset_for_tuple(None, first_path).unwrap(); + let len = first_source.len(); + merge_list.push_back(&first_source[min(offset, len)..]); + + // Now merge all the views into one singular String to return + 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); + + LFString::from_unchecked(merged) + } + + fn create_merge_views(&mut self, merge_list: &mut LinkedList<&'a str>, stack: &mut VecDeque<&'a NormalizedPathBuf>) { + loop { + let n = match self.nodes.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); + let parent_path = &parent.path; + let child_path = &child.path; + // 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(FilialTuple { + child: &n.child.path, + parent: n.parent.map(|p| &p.path), + }) + .or_insert_with(|| { + // let child_positions = self.graph.get_edges_between(parent, child); + Box::new(parent.includes_of_path(child_path).unwrap()) + }) + .next() + .unwrap(); + + let parent_source = &parent.source; + let (char_for_line, char_following_line) = self.char_offset_for_line(edge, parent_source); + + let offset = *self + .set_last_offset_for_tuple(stack.back().copied(), parent_path, char_following_line) + .get_or_insert(0); + + debug!("creating view to start child file"; + "parent" => parent_path, "child" => child_path, + "grandparent" => stack.back(), + "last_parent_offset" => offset, "line" => edge, "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, merge_list); + + match self.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.parent.unwrap().path != child_path { + let child_source = &child.source; + // if ends in \n\n, we want to exclude the last \n for some reason. Ask optilad + let double_newline_offset = match child_source.ends_with('\n') { + true => child_source.len() - 1, + false => child_source.len(), + }; + merge_list.push_back(&child_source[..double_newline_offset]); + self.set_last_offset_for_tuple(Some(parent_path), child_path, 0); + // +1 because edge.line is 0 indexed ~~but #line is 1 indexed and references the *following* line~~ + // turns out #line _is_ 0 indexed too? Im really confused + self.add_closing_line_directive(edge + self.get_line_directive_offset(), parent_path, merge_list); + // if the next pair's parent is not the current pair's parent, we need to bubble up + if stack.contains(&&next.parent.unwrap().path) { + return; + } + continue; + } + + stack.push_back(&parent.path); + self.create_merge_views(merge_list, stack); + stack.pop_back(); + + let offset = self.get_last_offset_for_tuple(Some(parent_path), child_path).unwrap(); + let child_source = &child.source; + // 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_path), child_path, 0); + } + + // +1 because edge.line is 0 indexed ~~but #line is 1 indexed and references the *following* line~~ + // turns out #line _is_ 0 indexed too? Im really confused + self.add_closing_line_directive(edge + self.get_line_directive_offset(), parent_path, merge_list); + + // we need to check the next item at the point of original return further down the callstack + if self.nodes.peek().is_some() && stack.contains(&&self.nodes.peek().unwrap().parent.unwrap().path) { + return; + } + } + None => { + // let child_source = self.sources.get(child_path).unwrap(); + let child_source = &child.source; + // if ends in \n\n, we want to exclude the last \n for some reason. Ask optilad + let double_newline_offset = match child_source.ends_with('\n') { + true => child_source.len() - 1, + false => child_source.len(), + }; + merge_list.push_back(&child_source[..double_newline_offset]); + self.set_last_offset_for_tuple(Some(parent_path), child_path, 0); + // +1 because edge.line is 0 indexed ~~but #line is 1 indexed and references the *following* line~~ + // turns out #line _is_ 0 indexed too? Im really confused + self.add_closing_line_directive(edge + self.get_line_directive_offset(), parent_path, merge_list); + } + } + } + } + + fn set_last_offset_for_tuple( + &mut self, parent: Option<&'a NormalizedPathBuf>, child: &'a NormalizedPathBuf, offset: usize, + ) -> Option { + debug!("inserting last offset"; + "parent" => parent, + "child" => &child, + "offset" => offset); + self.last_offset_set.insert(FilialTuple { child, parent }, offset) + } + + #[inline] + fn get_last_offset_for_tuple(&self, parent: Option<&'a NormalizedPathBuf>, child: &'a NormalizedPathBuf) -> Option { + 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: impl Into + Copy, 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.into() { + 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) + } + + #[inline] + fn find_version_offset(&self, source: &str) -> usize { + source + .lines() + .enumerate() + .find(|(_, line)| line.starts_with("#version ")) + .map_or(0, |(i, _)| i) + } + + #[inline] + fn get_line_directive_offset(&self) -> usize { + match (self.gpu_vendor, self.document_glsl_version) { + (opengl::GPUVendor::NVIDIA, Version::Glsl110) + | (opengl::GPUVendor::NVIDIA, Version::Glsl120) + | (opengl::GPUVendor::NVIDIA, Version::Glsl130) + | (opengl::GPUVendor::NVIDIA, Version::Glsl140) + | (opengl::GPUVendor::NVIDIA, Version::Glsl150) => 1, + _ => 0, + } + } + + fn add_preamble( + &mut self, version_line_offset: impl Into, version_char_offset: usize, path: &NormalizedPathBuf, source: &'a str, + merge_list: &mut LinkedList<&'a str>, + ) { + merge_list.push_back(&source[..version_char_offset]); + self.extra_lines.push(consts::OPTIFINE_PREAMBLE.into()); + self.unsafe_get_and_insert(merge_list); + self.add_closing_line_directive(version_line_offset.into() + self.get_line_directive_offset(), path, merge_list); + } + + fn add_opening_line_directive(&mut self, path: &NormalizedPathBuf, merge_list: &mut LinkedList<&str>) { + let line_directive = format!("#line 0 {} // {}\n", self.source_mapper.get_num(path), path); + self.extra_lines.push(line_directive); + self.unsafe_get_and_insert(merge_list); + } + + fn add_closing_line_directive(&mut self, line: impl Into, path: &NormalizedPathBuf, merge_list: &mut LinkedList<&str>) { + // 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.into(), self.source_mapper.get_num(path), path) + } else { + format!("\n#line {} {} // {}\n", line.into(), self.source_mapper.get_num(path), path) + } + } else { + format!("\n#line {} {} // {}\n", line.into(), self.source_mapper.get_num(path), path) + }; + + self.extra_lines.push(line_directive); + self.unsafe_get_and_insert(merge_list); + } + + fn unsafe_get_and_insert(&self, merge_list: &mut LinkedList<&str>) { + // :^) + unsafe { + let vec_ptr_offset = self.extra_lines.as_ptr().add(self.extra_lines.len() - 1); + merge_list.push_back(&vec_ptr_offset.as_ref().unwrap()[..]); + } + } +} + +#[cfg(test)] +mod test { + use std::{ + fs, + path::{Path, PathBuf}, + }; + + use anyhow::Result; + + use filesystem::{LFString, NormalizedPathBuf}; + use fs_extra::{copy_items, dir}; + use opengl::GPUVendor; + use pretty_assertions::assert_str_eq; + use sourcefile::{SourceMapper, Version}; + use tempdir::TempDir; + use workspace_tree::{TreeError, WorkspaceTree}; + + use crate::MergeViewBuilder; + + fn copy_to_tmp_dir(test_path: &str) -> (TempDir, NormalizedPathBuf) { + let tmp_dir = TempDir::new("mcshader").unwrap(); + fs::create_dir(tmp_dir.path().join("shaders")).unwrap(); + + { + let test_path = Path::new(test_path) + .canonicalize() + .unwrap_or_else(|_| panic!("canonicalizing '{}'", test_path)); + let opts = &dir::CopyOptions::new(); + let files = fs::read_dir(&test_path) + .unwrap() + .map(|e| String::from(e.unwrap().path().to_str().unwrap())) + .collect::>(); + copy_items(&files, &tmp_dir.path().join("shaders"), opts).unwrap(); + } + + let tmp_path = tmp_dir.path().to_str().unwrap().into(); + + (tmp_dir, tmp_path) + } + + #[tokio::test(flavor = "multi_thread", worker_threads = 1)] + #[logging_macro::scope] + async fn test_generate_merge_list_01() { + let (_tmp_dir, tmp_path) = copy_to_tmp_dir("../testdata/01"); + let mut workspace = WorkspaceTree::new(&tmp_path.clone()); + workspace.build(); + + let final_path = tmp_path.join("shaders").join("final.fsh"); + let common_path = tmp_path.join("shaders").join("common.glsl"); + + let mut trees_vec = workspace + .trees_for_entry(&final_path) + .expect("expected successful tree initializing") + .collect::, TreeError>>() + .expect("expected successful tree-building"); + let mut trees = trees_vec.iter_mut(); + + let tree = trees.next().unwrap(); + + assert!(trees.next().is_none()); + + let tree = tree + .collect::, TreeError>>() + .expect("expected successful tree-building"); + + let mut source_mapper = SourceMapper::new(2); + + let result = MergeViewBuilder::new(&tree, &mut source_mapper, GPUVendor::NVIDIA, Version::Glsl120).build(); + + let merge_file = tmp_path.join("shaders").join("final.fsh.merge"); + + let mut truth = LFString::read(merge_file).await.unwrap(); + truth = LFString::from_unchecked(truth.replacen("!!", &final_path.to_string(), 1)); + truth = LFString::from_unchecked(truth.replacen("!!", &common_path.to_string(), 1)); + truth = LFString::from_unchecked(truth.replace("!!", &final_path.to_string())); + + assert_str_eq!(*truth, *result); + } + + #[tokio::test(flavor = "multi_thread", worker_threads = 1)] + #[logging_macro::scope] + async fn test_generate_merge_list_02() { + let (_tmp_dir, tmp_path) = copy_to_tmp_dir("../testdata/02"); + let mut workspace = WorkspaceTree::new(&tmp_path.clone()); + workspace.build(); + + // println!( + // "connected {}. leaf {}", + // workspace.num_connected_entries(), + // // workspace.num_disconnected_entries(), + // ); + + let final_path = tmp_path.join("shaders").join("final.fsh"); + + let mut trees_vec = workspace + .trees_for_entry(&final_path) + .expect("expected successful tree initializing") + .collect::, TreeError>>() + .expect("expected successful tree-building"); + let mut trees = trees_vec.iter_mut(); + + let tree = trees.next().unwrap(); + + assert!(trees.next().is_none()); + + let tree = tree + .collect::, TreeError>>() + .expect("expected successful tree-building"); + + let mut source_mapper = SourceMapper::new(2); + + let result = MergeViewBuilder::new(&tree, &mut source_mapper, GPUVendor::NVIDIA, Version::Glsl120).build(); + + let merge_file = tmp_path.join("shaders").join("final.fsh.merge"); + + let mut truth = LFString::read(merge_file).await.unwrap(); + + truth = LFString::from_unchecked(truth.replacen("!!", &tmp_path.join("shaders").join("final.fsh").to_string(), 1)); + for file in &["sample.glsl", "burger.glsl", "sample.glsl", "test.glsl", "sample.glsl"] { + let path = tmp_path.clone(); + truth = LFString::from_unchecked(truth.replacen("!!", &path.join("shaders").join("utils").join(file).to_string(), 1)); + } + truth = LFString::from_unchecked(truth.replacen("!!", &tmp_path.join("shaders").join("final.fsh").to_string(), 1)); + + assert_str_eq!(*truth, *result); + } + + #[tokio::test(flavor = "multi_thread", worker_threads = 1)] + #[logging_macro::scope] + async fn test_generate_merge_list_03() { + let (_tmp_dir, tmp_path) = copy_to_tmp_dir("../testdata/03"); + let mut workspace = WorkspaceTree::new(&tmp_path.clone()); + workspace.build(); + + let final_path = tmp_path.join("shaders").join("final.fsh"); + + let mut trees_vec = workspace + .trees_for_entry(&final_path) + .expect("expected successful tree initializing") + .collect::, TreeError>>() + .expect("expected successful tree-building"); + let mut trees = trees_vec.iter_mut(); + + let tree = trees.next().unwrap(); + + assert!(trees.next().is_none()); + + let tree = tree + .collect::, TreeError>>() + .expect("expected successful tree-building"); + + let mut source_mapper = SourceMapper::new(2); + + let result = MergeViewBuilder::new(&tree, &mut source_mapper, GPUVendor::NVIDIA, Version::Glsl120).build(); + + let merge_file = tmp_path.join("shaders").join("final.fsh.merge"); + + let mut truth = LFString::read(merge_file).await.unwrap(); + + truth = LFString::from_unchecked(truth.replacen("!!", &tmp_path.join("shaders").join("final.fsh").to_string(), 1)); + for file in &["sample.glsl", "burger.glsl", "sample.glsl", "test.glsl", "sample.glsl"] { + let path = tmp_path.clone(); + truth = LFString::from_unchecked(truth.replacen("!!", &path.join("shaders").join("utils").join(file).to_string(), 1)); + } + truth = LFString::from_unchecked(truth.replacen("!!", &tmp_path.join("shaders").join("final.fsh").to_string(), 1)); + + assert_str_eq!(*truth, *result); + } + + #[tokio::test(flavor = "multi_thread", worker_threads = 1)] + #[logging_macro::scope] + async fn test_generate_merge_list_04() { + let (_tmp_dir, tmp_path) = copy_to_tmp_dir("../testdata/04"); + let mut workspace = WorkspaceTree::new(&tmp_path.clone()); + workspace.build(); + + let final_path = tmp_path.join("shaders").join("final.fsh"); + + let mut trees_vec = workspace + .trees_for_entry(&final_path) + .expect("expected successful tree initializing") + .collect::, TreeError>>() + .expect("expected successful tree-building"); + let mut trees = trees_vec.iter_mut(); + + let tree = trees.next().unwrap(); + + assert!(trees.next().is_none()); + + let tree = tree + .collect::, TreeError>>() + .expect("expected successful tree-building"); + + let mut source_mapper = SourceMapper::new(2); + + let result = MergeViewBuilder::new(&tree, &mut source_mapper, GPUVendor::NVIDIA, Version::Glsl120).build(); + + let merge_file = tmp_path.join("shaders").join("final.fsh.merge"); + + let mut truth = LFString::read(merge_file).await.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 = LFString::from_unchecked(truth.replacen("!!", &path.join("shaders").join(file).to_string(), 1)); + } + + assert_str_eq!(*truth, *result); + } + + #[tokio::test(flavor = "multi_thread", worker_threads = 1)] + #[logging_macro::scope] + async fn test_generate_merge_list_06() { + let (_tmp_dir, tmp_path) = copy_to_tmp_dir("../testdata/06"); + let mut workspace = WorkspaceTree::new(&tmp_path.clone()); + workspace.build(); + + let final_path = tmp_path.join("shaders").join("final.fsh"); + + let mut trees_vec = workspace + .trees_for_entry(&final_path) + .expect("expected successful tree initializing") + .collect::, TreeError>>() + .expect("expected successful tree-building"); + let mut trees = trees_vec.iter_mut(); + + let tree = trees.next().unwrap(); + + assert!(trees.next().is_none()); + + let tree = tree + .collect::, TreeError>>() + .expect("expected successful tree-building"); + + let mut source_mapper = SourceMapper::new(2); + + let result = MergeViewBuilder::new(&tree, &mut source_mapper, GPUVendor::NVIDIA, Version::Glsl120).build(); + + let merge_file = tmp_path.join("shaders").join("final.fsh.merge"); + + let mut truth = LFString::read(merge_file).await.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 = LFString::from_unchecked(truth.replacen("!!", &path.join("shaders").join(file).to_string(), 1)); + } + + assert_str_eq!(*truth, *result); + } +} diff --git a/server/logging/Cargo.toml b/server/logging/Cargo.toml index 3714c9e..7f01c26 100644 --- a/server/logging/Cargo.toml +++ b/server/logging/Cargo.toml @@ -4,10 +4,19 @@ version = "0.9.8" authors = ["Noah Santschi-Cooney "] edition = "2021" +[lib] +doctest = false + [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" +slog-async = "2.7.0" +slog-stdlog = "4.1.1" +slog-scope-futures = "0.1.1" +log = "0.4.16" rand = "0.8" -lazy_static = "1.4" \ No newline at end of file +lazy_static = "1.4" +tokio = { version = "1.18.0", features = ["full"] } +logging_macro = { path = "../logging_macro" } \ No newline at end of file diff --git a/server/logging/src/lib.rs b/server/logging/src/lib.rs index 36d7f53..6a82864 100644 --- a/server/logging/src/lib.rs +++ b/server/logging/src/lib.rs @@ -1,6 +1,4 @@ use rand::{rngs, Rng}; -use slog::slog_o; -use slog_scope::GlobalLoggerGuard; use slog_term::{FullFormat, PlainSyncDecorator}; use std::{cell::RefCell, sync::Arc}; @@ -10,16 +8,18 @@ use lazy_static::lazy_static; use slog::*; use slog_atomic::*; -fn new_trace_id() -> String { +pub use logging_macro::*; +pub use slog_scope::{scope, logger, error, warn, info, trace, debug, GlobalLoggerGuard}; +pub use slog::{slog_o, FnValue, Level, Value, Record, Key, Serializer, Result}; +pub use slog_scope_futures::FutureExt; + +pub 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: F) { - slog_scope::scope(&slog_scope::logger().new(slog_o!("trace" => new_trace_id())), f) -} - -pub fn set_logger_with_level(level: Level) -> GlobalLoggerGuard { +pub fn set_level(level: Level) -> GlobalLoggerGuard { + slog_stdlog::init_with_level(log::Level::Trace).err().or(None); let drain = Arc::new(logger_base(level).fuse()); DRAIN_SWITCH.ctrl().set(drain.clone()); slog_scope::set_global_logger(Logger::root(drain, o!())) diff --git a/server/logging_macro/Cargo.toml b/server/logging_macro/Cargo.toml index efce6d8..a4b6362 100644 --- a/server/logging_macro/Cargo.toml +++ b/server/logging_macro/Cargo.toml @@ -6,6 +6,7 @@ edition = "2021" [lib] proc-macro = true +doctest = false [dependencies] quote = "1.0" diff --git a/server/logging_macro/src/lib.rs b/server/logging_macro/src/lib.rs index 99052bb..ca6f5fb 100644 --- a/server/logging_macro/src/lib.rs +++ b/server/logging_macro/src/lib.rs @@ -3,22 +3,36 @@ use quote::quote; use syn::{parse_macro_input, parse_quote, ItemFn}; #[proc_macro_attribute] -pub fn log_scope(_args: TokenStream, function: TokenStream) -> TokenStream { +pub fn 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 logging::{slog_o, FnValue, Level, scope, logger}; 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())))), || { + let _guard = logging::set_level(Level::Trace); + scope(&logger().new(slog_o!("test_name" => #function_name, "thread_num" => FnValue(|_| format!("{:?}", current().id())))), || { #(#stmts)* }); })); TokenStream::from(quote!(#function)) } + +#[proc_macro_attribute] +pub fn with_trace_id(_args: TokenStream, function: TokenStream) -> TokenStream { + let mut function = parse_macro_input!(function as ItemFn); + let stmts = function.block.stmts; + + function.block = Box::new(parse_quote!({ + use logging::{slog_o, scope, logger, new_trace_id}; + + scope(&logger().new(slog_o!("trace" => new_trace_id())), || { + #(#stmts)* + }) + })); + + TokenStream::from(quote!(#function)) +} diff --git a/server/main/Cargo.toml b/server/main/Cargo.toml index 824a69b..e3954b2 100644 --- a/server/main/Cargo.toml +++ b/server/main/Cargo.toml @@ -5,31 +5,42 @@ authors = ["Noah Santschi-Cooney "] 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" +graph = { path = "../graph" } 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" +glob = "0.3" +filesystem = { path = "../filesystem" } + +# glutin = "0.28" +gl = "0.14" + +anyhow = "1.0" +thiserror = "1.0" + +tree-sitter = "0.20" +tree-sitter-glsl = "0.1" logging = { path = "../logging" } logging_macro = { path = "../logging_macro" } +server = { path = "../server" } +tower-lsp = "0.17" +tokio = { version = "1.18", features = ["full"] } +futures = "0.3" + +workspace = { path = "../workspace" } +opengl = { path = "../opengl" } +sourcefile = { path = "../sourcefile" } + [dev-dependencies] tempdir = "0.3" fs_extra = "1.2" hamcrest2 = "*" -pretty_assertions = "1.1" \ No newline at end of file +pretty_assertions = "1.1" +tower-test = "0.4" \ No newline at end of file diff --git a/server/main/src/commands/graph_dot.rs b/server/main/src/commands/graph_dot.rs deleted file mode 100644 index dac35e7..0000000 --- a/server/main/src/commands/graph_dot.rs +++ /dev/null @@ -1,52 +0,0 @@ -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>, -} - -impl Invokeable for GraphDotCommand { - fn run_command(&self, root: &Path, _: &[Value]) -> Result { - 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), - } - } -} diff --git a/server/main/src/commands/merged_includes.rs b/server/main/src/commands/merged_includes.rs deleted file mode 100644 index d932213..0000000 --- a/server/main/src/commands/merged_includes.rs +++ /dev/null @@ -1,114 +0,0 @@ -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>, -} - -impl VirtualMergedDocument { - // TODO: DUPLICATE CODE - fn get_file_toplevel_ancestors(&self, uri: &Path) -> Result>> { - 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, dfs::error::CycleError> { - let graph_ref = self.graph.borrow(); - - let dfs = dfs::Dfs::new(&graph_ref, root); - - dfs.collect::, _>>() - } - - pub fn load_sources(&self, nodes: &[FilialTuple]) -> Result> { - 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 { - 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::>()); - - // the set of all filepath->content. TODO: change to Url? - let mut all_sources: HashMap = 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() - )); - } -} diff --git a/server/main/src/commands/mod.rs b/server/main/src/commands/mod.rs deleted file mode 100644 index 0bb17f1..0000000 --- a/server/main/src/commands/mod.rs +++ /dev/null @@ -1,36 +0,0 @@ -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>, -} - -impl CustomCommandProvider { - pub fn new(commands: Vec<(&str, Box)>) -> 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 { - if self.commands.contains_key(command) { - info!("running command"; - "command" => command, - "args" => format!("[{}]", args.iter().map(|v| serde_json::to_string(v).unwrap()).collect::>().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; -} diff --git a/server/main/src/configuration.rs b/server/main/src/configuration.rs index 779b235..95c5bc4 100644 --- a/server/main/src/configuration.rs +++ b/server/main/src/configuration.rs @@ -1,8 +1,6 @@ use std::str::FromStr; -use slog::Level; -use slog_scope::error; - +use logging::{Level, error}; pub fn handle_log_level_change(log_level: String, callback: F) { match Level::from_str(log_level.as_str()) { diff --git a/server/main/src/consts.rs b/server/main/src/consts.rs deleted file mode 100644 index 282448d..0000000 --- a/server/main/src/consts.rs +++ /dev/null @@ -1,4 +0,0 @@ -pub static SOURCE: &str = "mc-glsl"; - -#[allow(dead_code)] -pub static INCLUDE_DIRECTIVE: &str = "#extension GL_GOOGLE_include_directive : require\n"; \ No newline at end of file diff --git a/server/main/src/dfs.rs b/server/main/src/dfs.rs deleted file mode 100644 index 11d3a6f..0000000 --- a/server/main/src/dfs.rs +++ /dev/null @@ -1,335 +0,0 @@ -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, - graph: &'a CachedStableGraph, - cycle: Vec, -} - -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 = 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; - - fn next(&mut self) -> Option> { - 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); - - impl StdError for CycleError {} - - impl CycleError { - pub fn new(nodes: &[NodeIndex], current_node: NodeIndex, graph: &CachedStableGraph) -> Self { - let mut resolved_nodes: Vec = 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 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 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 = collection.iter().map(|n| n.child).collect(); - let parents: Vec> = 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 = collection.iter().map(|n| n.child).collect(); - let parents: Vec> = 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()); - } - } -} diff --git a/server/main/src/graph.rs b/server/main/src/graph.rs deleted file mode 100644 index 608a299..0000000 --- a/server/main/src/graph.rs +++ /dev/null @@ -1,374 +0,0 @@ -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, - cache: HashMap, - // Maps a node index to its abstracted string representation. - // Mainly used as the graph is based on NodeIndex. - reverse_index: HashMap, -} - -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 { - 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 + '_ { - 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::>(); - 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 + '_ { - 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::>(); - 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 + '_ { - self.graph.neighbors(node) - } - - pub fn collect_root_ancestors(&self, node: NodeIndex) -> Vec { - let mut visited = HashSet::new(); - self.get_root_ancestors(node, node, &mut visited) - } - - // TODO: impl Iterator - fn parent_node_indexes(&self, node: NodeIndex) -> Vec { - self.graph.neighbors_directed(node, Direction::Incoming).collect() - } - - fn get_root_ancestors(&self, initial: NodeIndex, node: NodeIndex, visited: &mut HashSet) -> Vec { - 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 { - 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 { - 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::::into("banana".to_string())); - - let children: Vec = 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::::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![]); - } - } -} diff --git a/server/main/src/lsp_ext.rs b/server/main/src/lsp_ext.rs deleted file mode 100644 index bcf1198..0000000 --- a/server/main/src/lsp_ext.rs +++ /dev/null @@ -1,16 +0,0 @@ -use rust_lsp::lsp_types::notification::Notification; -use serde::{Deserialize, Serialize}; - -pub enum Status {} - -impl Notification for Status { - type Params = StatusParams; - const METHOD: &'static str = "mc-glsl/status"; -} - -#[derive(Debug, Eq, PartialEq, Clone, Deserialize, Serialize)] -pub struct StatusParams { - pub status: String, - pub message: Option, - pub icon: Option, -} diff --git a/server/main/src/main.rs b/server/main/src/main.rs index b151174..2e9f115 100644 --- a/server/main/src/main.rs +++ b/server/main/src/main.rs @@ -1,933 +1,20 @@ -#![feature(once_cell)] #![feature(option_get_or_insert_default)] +use logging::{logger, FutureExt}; +use server::Server; +use tower_lsp::LspService; -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 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; +#[tokio::main] +async fn main() { + let _guard = logging::set_level(logging::Level::Debug); -lazy_static! { - static ref RE_INCLUDE: Regex = Regex::new(r#"^(?:\s)*?(?:#include) "(.+)"\r?"#).unwrap(); - static ref TOPLEVEL_FILES: HashSet = { - let mut set = HashSet::with_capacity(6864); - for folder in ["shaders/", "shaders/world0/", "shaders/world1/", "shaders/world-1/"] { - for ext in ["fsh", "vsh", "gsh", "csh"] { - set.insert(format!("{}composite.{}", folder, ext).into()); - for i in 1..=99 { - set.insert(format!("{}composite{}.{}", folder, i, ext).into()); - set.insert(format!("{}deferred{}.{}", folder, i, ext).into()); - set.insert(format!("{}prepare{}.{}", folder, i, ext).into()); - set.insert(format!("{}shadowcomp{}.{}", folder, i, ext).into()); - } - set.insert(format!("{}composite_pre.{}", folder, ext).into()); - set.insert(format!("{}deferred.{}", folder, ext).into()); - set.insert(format!("{}deferred_pre.{}", folder, ext).into()); - set.insert(format!("{}final.{}", folder, ext).into()); - set.insert(format!("{}gbuffers_armor_glint.{}", folder, ext).into()); - set.insert(format!("{}gbuffers_basic.{}", folder, ext).into()); - set.insert(format!("{}gbuffers_beaconbeam.{}", folder, ext).into()); - set.insert(format!("{}gbuffers_block.{}", folder, ext).into()); - set.insert(format!("{}gbuffers_clouds.{}", folder, ext).into()); - set.insert(format!("{}gbuffers_damagedblock.{}", folder, ext).into()); - set.insert(format!("{}gbuffers_entities.{}", folder, ext).into()); - set.insert(format!("{}gbuffers_entities_glowing.{}", folder, ext).into()); - set.insert(format!("{}gbuffers_hand.{}", folder, ext).into()); - set.insert(format!("{}gbuffers_hand.{}", folder, ext).into()); - set.insert(format!("{}gbuffers_hand_water.{}", folder, ext).into()); - set.insert(format!("{}gbuffers_item.{}", folder, ext).into()); - set.insert(format!("{}gbuffers_line.{}", folder, ext).into()); - set.insert(format!("{}gbuffers_skybasic.{}", folder, ext).into()); - set.insert(format!("{}gbuffers_skytextured.{}", folder, ext).into()); - set.insert(format!("{}gbuffers_spidereyes.{}", folder, ext).into()); - set.insert(format!("{}gbuffers_terrain.{}", folder, ext).into()); - set.insert(format!("{}gbuffers_terrain_cutout.{}", folder, ext).into()); - set.insert(format!("{}gbuffers_terrain_cutout_mip.{}", folder, ext).into()); - set.insert(format!("{}gbuffers_terrain_solid.{}", folder, ext).into()); - set.insert(format!("{}gbuffers_textured.{}", folder, ext).into()); - set.insert(format!("{}gbuffers_textured_lit.{}", folder, ext).into()); - set.insert(format!("{}gbuffers_water.{}", folder, ext).into()); - set.insert(format!("{}gbuffers_weather.{}", folder, ext).into()); - set.insert(format!("{}prepare.{}", folder, ext).into()); - set.insert(format!("{}shadow.{}", folder, ext).into()); - set.insert(format!("{}shadow_cutout.{}", folder, ext).into()); - set.insert(format!("{}shadow_solid.{}", folder, ext).into()); - set.insert(format!("{}shadowcomp.{}", folder, ext).into()); - } - } - 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>, - root: PathBuf, - command_provider: Option, - opengl_context: Rc, - tree_sitter: Rc>, - log_guard: Option, -} - -#[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) -> MethodError { - let msg = "Functionality not implemented.".to_string(); - MethodError:: { - 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>> { - // 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::>()) - ); - - // the set of all filepath->content. - let mut all_sources: HashMap = HashMap::new(); - // the set of filepath->list of diagnostics to report - let mut diagnostics: HashMap> = HashMap::new(); - - // we want to backfill the diagnostics map with all linked sources - let back_fill = |all_sources: &HashMap, diagnostics: &mut HashMap>| { - 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 !TOPLEVEL_FILES.contains(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)> = 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 !TOPLEVEL_FILES.contains(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 { - 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, dfs::error::CycleError> { - let graph_ref = self.graph.borrow(); - - let dfs = dfs::Dfs::new(&graph_ref, root); - - dfs.collect::>() - } - - pub fn load_sources(&self, nodes: &[FilialTuple]) -> Result> { - 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>> { - 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>, document_version: Option) { - // 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, message: impl Into, icon: impl Into) { - 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) { - 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, - } - - let config: Configuration = from_value(params.settings.as_object().unwrap().get("mcglsl").unwrap().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) { - completable.complete(Err(Self::error_not_available(()))); - } - - fn resolve_completion_item(&mut self, _: CompletionItem, completable: LSCompletable) { - completable.complete(Err(Self::error_not_available(()))); - } - - fn hover(&mut self, _: TextDocumentPositionParams, _: LSCompletable) { - /* 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>) { - 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) { - completable.complete(Err(Self::error_not_available(()))); - } - - fn goto_definition(&mut self, params: TextDocumentPositionParams, completable: LSCompletable>) { - 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>) { - 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>) { - completable.complete(Err(Self::error_not_available(()))); - } - - fn document_symbols(&mut self, params: DocumentSymbolParams, completable: LSCompletable) { - 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) { - completable.complete(Err(Self::error_not_available(()))); - } - - fn code_action(&mut self, _: CodeActionParams, completable: LSCompletable>) { - completable.complete(Err(Self::error_not_available(()))); - } - - fn code_lens(&mut self, _: CodeLensParams, completable: LSCompletable>) { - completable.complete(Err(Self::error_not_available(()))); - } - - fn code_lens_resolve(&mut self, _: CodeLens, completable: LSCompletable) { - completable.complete(Err(Self::error_not_available(()))); - } - - fn document_link(&mut self, params: DocumentLinkParams, completable: LSCompletable>) { - 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 = self - .graph - .borrow() - .child_node_indexes(node) - .filter_map::, _>(|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::>()), - "path" => curr_doc.to_str().unwrap(), - ); - completable.complete(Ok(edges)); - }); - } - - fn document_link_resolve(&mut self, _: DocumentLink, completable: LSCompletable) { - completable.complete(Err(Self::error_not_available(()))); - } - - fn formatting(&mut self, _: DocumentFormattingParams, completable: LSCompletable>) { - completable.complete(Err(Self::error_not_available(()))); - } - - fn range_formatting(&mut self, _: DocumentRangeFormattingParams, completable: LSCompletable>) { - completable.complete(Err(Self::error_not_available(()))); - } - - fn on_type_formatting(&mut self, _: DocumentOnTypeFormattingParams, completable: LSCompletable>) { - completable.complete(Err(Self::error_not_available(()))); - } - - fn rename(&mut self, _: RenameParams, completable: LSCompletable) { - completable.complete(Err(Self::error_not_available(()))); - } + let stdin = tokio::io::stdin(); + let stdout = tokio::io::stdout(); + let (service, socket) = LspService::new(|client| Server::new(client, opengl::ContextFacade::default)); + tower_lsp::Server::new(stdin, stdout, socket) + .serve(service) + .with_logger(logger()) + .await; } diff --git a/server/main/src/merge_views.rs b/server/main/src/merge_views.rs deleted file mode 100644 index 8eec146..0000000 --- a/server/main/src/merge_views.rs +++ /dev/null @@ -1,645 +0,0 @@ -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, -} - -/// 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>, - - sources: &'a HashMap, - 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, - // 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 + 'a)>>, -} - -impl<'a> MergeViewBuilder<'a> { - pub fn new( - nodes: &'a [FilialTuple], sources: &'a HashMap, 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 = 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::::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, stack: &mut VecDeque) { - 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, child: NodeIndex, offset: usize) -> Option { - 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, child: NodeIndex) -> Option { - 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, 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, - ) { - 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, - ) { - // 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); - } -} diff --git a/server/main/src/navigation.rs b/server/main/src/navigation.rs index 9418e86..5f8312e 100644 --- a/server/main/src/navigation.rs +++ b/server/main/src/navigation.rs @@ -1,13 +1,12 @@ 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 logging::{trace, debug, info}; +use sourcefile::LineMap; +use tower_lsp::lsp_types::{DocumentSymbol, Location, Position, Range, SymbolKind}; 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); @@ -53,8 +52,8 @@ impl SymbolName { } } -impl slog::Value for SymbolName { - fn serialize(&self, record: &slog::Record, key: slog::Key, serializer: &mut dyn slog::Serializer) -> slog::Result { +impl logging::Value for SymbolName { + fn serialize(&self, record: &logging::Record, key: logging::Key, serializer: &mut dyn logging::Serializer) -> logging::Result { self.0.serialize(record, key, serializer) } } @@ -161,7 +160,7 @@ impl<'a> ParserContext<'a> { }) } - pub fn list_symbols(&self, _path: &Path) -> Result>> { + pub fn list_document_symbols(&self, _path: &Path) -> Result>> { let query = Query::new(tree_sitter_glsl::language(), LIST_SYMBOLS_STR)?; let mut query_cursor = QueryCursor::new(); @@ -222,7 +221,6 @@ impl<'a> ParserContext<'a> { 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; diff --git a/server/main/src/test.rs b/server/main/src/test.rs deleted file mode 100644 index afb4761..0000000 --- a/server/main/src/test.rs +++ /dev/null @@ -1,281 +0,0 @@ -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, -} - -impl io::Write for StdoutNewline { - fn write(&mut self, buf: &[u8]) -> Result { - 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>) -> 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::>(); - copy_items(&files, dest.path().join("shaders"), opts).unwrap(); -} - -pub fn copy_to_and_set_root(test_path: &str, server: &mut MinecraftShaderLanguageServer) -> (Rc, 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, 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| { - 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| { - 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| { - 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); - } -} diff --git a/server/main/src/url_norm.rs b/server/main/src/url_norm.rs deleted file mode 100644 index 67fe813..0000000 --- a/server/main/src/url_norm.rs +++ /dev/null @@ -1,73 +0,0 @@ -use std::path::PathBuf; - -use slog_scope::trace; -use anyhow::Result; -use path_slash::PathBufExt; -use url::Url; - -pub trait FromUrl { - fn from_url(u: Url) -> Self; -} - -pub trait FromJson { - fn from_json(v: &serde_json::value::Value) -> Result - where - Self: Sized; -} - -impl FromUrl for PathBuf { - #[cfg(target_family = "windows")] - fn from_url(u: Url) -> Self { - 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) - } - - #[cfg(target_family = "unix")] - fn from_url(u: Url) -> Self { - 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) - } -} - -impl FromJson for PathBuf { - #[cfg(target_family = "windows")] - fn from_json(v: &serde_json::value::Value) -> Result - where - Self: Sized, - { - if !v.is_string() { - return Err(anyhow::format_err!("cannot convert {:?} to PathBuf", v)); - } - let path = v.to_string(); - let path = percent_encoding::percent_decode_str(path.trim_start_matches('"').trim_end_matches('"').strip_prefix('/').unwrap()) - .decode_utf8()?; - - trace!("converted win path from json"; "old" => v.to_string(), "new" => path.to_string()); - - Ok(PathBuf::from_slash(path)) - } - - #[cfg(target_family = "unix")] - fn from_json(v: &serde_json::value::Value) -> Result - where - Self: Sized, - { - if !v.is_string() { - return Err(anyhow::format_err!("cannot convert {:?} to PathBuf", v)); - } - let path = v.to_string(); - let path = percent_encoding::percent_decode_str(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)) - } -} diff --git a/server/main/testdata/01/final.fsh.merge b/server/main/testdata/01/final.fsh.merge deleted file mode 100644 index 7c606b6..0000000 --- a/server/main/testdata/01/final.fsh.merge +++ /dev/null @@ -1,11 +0,0 @@ -#version 120 - -#line 1 1 // !! -float test() { - return 0.5; -} -#line 4 0 // !! - -void main() { - gl_FragColor[0] = vec4(0.0); -} \ No newline at end of file diff --git a/server/main/testdata/02/final.fsh.merge b/server/main/testdata/02/final.fsh.merge deleted file mode 100644 index 6e6d776..0000000 --- a/server/main/testdata/02/final.fsh.merge +++ /dev/null @@ -1,27 +0,0 @@ -#version 120 - -#line 1 1 // !! -int sample() { - return 5; -} - -#line 1 2 // !! -void burger() { - // sample text -} -#line 6 1 // !! - -#line 1 3 // !! -float test() { - return 3.0; -} -#line 8 1 // !! - -int sample_more() { - return 5; -} -#line 4 0 // !! - -void main() { - gl_FragColor = vec4(1.0, 1.0, 1.0, 1.0); -} diff --git a/server/main/testdata/03/final.fsh.merge b/server/main/testdata/03/final.fsh.merge deleted file mode 100644 index f317989..0000000 --- a/server/main/testdata/03/final.fsh.merge +++ /dev/null @@ -1,23 +0,0 @@ -#version 120 - -#line 1 1 // !! -int sample() { - return 5; -} - -#line 1 2 // !! -void burger() { - // sample text -} -#line 6 1 // !! - -#line 1 3 // !! -float test() { - return 3.0; -} -#line 8 1 // !! -#line 4 0 // !! - -void main() { - gl_FragColor = vec4(1.0, 1.0, 1.0, 1.0); -} diff --git a/server/main/testdata/04/final.fsh.merge b/server/main/testdata/04/final.fsh.merge deleted file mode 100644 index 49da4ac..0000000 --- a/server/main/testdata/04/final.fsh.merge +++ /dev/null @@ -1,23 +0,0 @@ -#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() { - -} \ No newline at end of file diff --git a/server/main/testdata/06/final.fsh.merge b/server/main/testdata/06/final.fsh.merge deleted file mode 100644 index 8ee023e..0000000 --- a/server/main/testdata/06/final.fsh.merge +++ /dev/null @@ -1,17 +0,0 @@ -#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() {} \ No newline at end of file diff --git a/server/opengl/Cargo.toml b/server/opengl/Cargo.toml new file mode 100644 index 0000000..4e0ae9f --- /dev/null +++ b/server/opengl/Cargo.toml @@ -0,0 +1,26 @@ +[package] +name = "opengl" +version = "0.9.8" +authors = ["Noah Santschi-Cooney "] +edition = "2021" + +[lib] +doctest = false + +[dependencies] +glutin = "0.28" +gl = "0.14" +url = "2.2" +filesystem = { path = "../filesystem" } +graph = { path = "../graph" } +tower-lsp = "0.17.0" +regex = "1.4" +mockall = "0.11" +logging = { path = "../logging" } +sourcefile = { path = "../sourcefile" } + +[dev-dependencies] +# workspace = { path = "../workspace" } +logging_macro = { path = "../logging_macro" } +tokio = { version = "1.18", features = ["fs"]} +trim-margin = "0.1" \ No newline at end of file diff --git a/server/main/src/diagnostics_parser.rs b/server/opengl/src/diagnostics_parser.rs similarity index 54% rename from server/main/src/diagnostics_parser.rs rename to server/opengl/src/diagnostics_parser.rs index 2c4279b..a99e688 100644 --- a/server/main/src/diagnostics_parser.rs +++ b/server/opengl/src/diagnostics_parser.rs @@ -1,27 +1,25 @@ -use std::{collections::HashMap, lazy::OnceCell, path::Path}; - +use std::collections::HashMap; +use core::cell::OnceCell; +use filesystem::NormalizedPathBuf; +use logging::debug; use regex::Regex; -use rust_lsp::lsp_types::{Diagnostic, DiagnosticSeverity, Position, Range}; -use slog_scope::debug; +use tower_lsp::lsp_types::*; +use tower_lsp::lsp_types::{Diagnostic, DiagnosticSeverity}; use url::Url; -use crate::{ - consts, - graph::CachedStableGraph, - opengl, - source_mapper::{SourceMapper, SourceNum}, -}; +use crate::ShaderValidator; +use sourcefile::{SourceMapper, SourceNum}; -pub struct DiagnosticsParser<'a, T: opengl::ShaderValidator + ?Sized> { - line_offset: OnceCell, +pub struct DiagnosticsParser<'a, T: ShaderValidator + ?Sized> { + // line_offset: OnceCell, line_regex: OnceCell, vendor_querier: &'a T, } -impl<'a, T: opengl::ShaderValidator + ?Sized> DiagnosticsParser<'a, T> { +impl<'a, T: ShaderValidator + ?Sized> DiagnosticsParser<'a, T> { pub fn new(vendor_querier: &'a T) -> Self { DiagnosticsParser { - line_offset: OnceCell::new(), + // line_offset: OnceCell::new(), line_regex: OnceCell::new(), vendor_querier, } @@ -32,20 +30,26 @@ impl<'a, T: opengl::ShaderValidator + ?Sized> DiagnosticsParser<'a, T> { "NVIDIA Corporation" => { Regex::new(r#"^(?P\d+)\((?P\d+)\) : (?Perror|warning) [A-C]\d+: (?P.+)"#).unwrap() } - _ => Regex::new(r#"^(?PERROR|WARNING): (?P[^?<>*|"\n]+):(?P\d+): (?:'.*' :|[a-z]+\(#\d+\)) +(?P.+)$"#) - .unwrap(), + _ => Regex::new( + r#"^(?PERROR|WARNING): (?P[^?<>*|"\n]+):(?P\d+): (?:'.*' :|[a-z]+\(#\d+\)) +(?P.+)$"#, + ) + .unwrap(), }) } - fn get_line_offset(&self) -> u32 { - *self.line_offset.get_or_init(|| match self.vendor_querier.vendor().as_str() { - "ATI Technologies" => 0, - _ => 1, - }) - } + // fn get_line_offset(&self) -> u32 { + // *self.line_offset.get_or_init(|| match self.vendor_querier.vendor().as_str() { + // "ATI Technologies" | "ATI Technologies Inc." | "AMD" => 0, + // _ => 1, + // }) + // } pub fn parse_diagnostics_output( - &self, output: String, uri: &Path, source_mapper: &SourceMapper, graph: &CachedStableGraph, + &self, + output: String, + uri: &NormalizedPathBuf, + source_mapper: &SourceMapper, + // graph: &CachedStableGraph, ) -> HashMap> { let output_lines = output.split('\n').collect::>(); let mut diagnostics: HashMap> = HashMap::with_capacity(output_lines.len()); @@ -65,7 +69,7 @@ impl<'a, T: opengl::ShaderValidator + ?Sized> DiagnosticsParser<'a, T> { let line = match diagnostic_capture.name("linenum") { Some(c) => c.as_str().parse::().unwrap_or(0), None => 0, - } - self.get_line_offset(); + }; // TODO: line matching maybe /* let line_text = source_lines[line as usize]; @@ -83,27 +87,22 @@ impl<'a, T: opengl::ShaderValidator + ?Sized> DiagnosticsParser<'a, T> { let origin = match diagnostic_capture.name("filepath") { Some(o) => { let source_num: SourceNum = o.as_str().parse::().unwrap().into(); - let graph_node = source_mapper.get_node(source_num); - graph.get_node(graph_node).to_str().unwrap().to_string() + source_mapper.get_node(source_num) } - None => uri.to_str().unwrap().to_string(), + None => uri, }; 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), + Position::new(line-1, 0), + Position::new(line-1, 1000), ), - code: None, severity: Some(severity), - source: Some(consts::SOURCE.into()), + source: Some("mcglsl".to_string()), message: msg.trim().into(), - related_information: None, - tags: None, - code_description: Option::None, - data: Option::None, + ..Default::default() }; let origin_url = Url::from_file_path(origin).unwrap(); @@ -120,75 +119,68 @@ impl<'a, T: opengl::ShaderValidator + ?Sized> DiagnosticsParser<'a, T> { #[cfg(test)] mod diagnostics_test { - use std::path::PathBuf; - - use slog::slog_o; + use filesystem::NormalizedPathBuf; + use sourcefile::SourceMapper; + use trim_margin::MarginTrimmable; use url::Url; - use crate::{ - diagnostics_parser::DiagnosticsParser, opengl::MockShaderValidator, source_mapper::SourceMapper, test::new_temp_server, - }; + use crate::{diagnostics_parser::DiagnosticsParser, MockShaderValidator}; #[test] - #[logging_macro::log_scope] + #[logging_macro::scope] fn test_nvidia_diagnostics() { - slog_scope::scope(&slog_scope::logger().new(slog_o!("driver" => "nvidia")), || { + logging::scope(&logging::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(); + let path: NormalizedPathBuf = "/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 path: NormalizedPathBuf = "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)); + source_mapper.get_num(&path); - let parser = DiagnosticsParser::new(server.opengl_context.as_ref()); + let parser = DiagnosticsParser::new(&mockgl); - let results = - parser.parse_diagnostics_output(output.to_string(), path.parent().unwrap(), &source_mapper, &server.graph.borrow()); + let results = parser.parse_diagnostics_output(output.to_string(), &path.parent().unwrap(), &source_mapper); 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] + #[logging_macro::scope] fn test_amd_diagnostics() { - slog_scope::scope(&slog_scope::logger().new(slog_o!("driver" => "amd")), || { + logging::scope(&logging::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 -"; + let output = r#" + |ERROR: 0:1: '' : syntax error: #line + |ERROR: 0:10: '' : syntax error: #line + |ERROR: 0:15: 'varying' : syntax error: syntax error + "#.trim_margin().unwrap(); #[cfg(target_family = "unix")] - let path: PathBuf = "/home/noah/.minecraft/shaderpacks/test/shaders/final.fsh".into(); + let path: NormalizedPathBuf = "/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 path: NormalizedPathBuf = "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)); + source_mapper.get_num(&path); - let parser = DiagnosticsParser::new(server.opengl_context.as_ref()); + let parser = DiagnosticsParser::new(&mockgl); - let results = - parser.parse_diagnostics_output(output.to_string(), path.parent().unwrap(), &source_mapper, &server.graph.borrow()); + let results = parser.parse_diagnostics_output(output, &path.parent().unwrap(), &source_mapper); assert_eq!(results.len(), 1); let first = results.into_iter().next().unwrap(); assert_eq!(first.1.len(), 3); - server.endpoint.request_shutdown(); }); } } diff --git a/server/opengl/src/lib.rs b/server/opengl/src/lib.rs new file mode 100644 index 0000000..c142d0c --- /dev/null +++ b/server/opengl/src/lib.rs @@ -0,0 +1,56 @@ +#![feature(once_cell)] +mod opengl_context; +mod opengl_context_facade; + +use mockall::automock; +use opengl_context::*; +pub use opengl_context_facade::*; + +pub mod diagnostics_parser; + +use std::fmt::Debug; + +#[automock] +pub trait ShaderValidator { + fn validate(&self, tree_type: TreeType, source: &str) -> Option; + fn vendor(&self) -> String; +} + +#[derive(Debug, Clone, Copy)] +pub enum GPUVendor { + NVIDIA, AMD, OTHER // and thats it folks +} + +impl From<&str> for GPUVendor { + fn from(s: &str) -> Self { + match s { + "NVIDIA Corporation" => Self::NVIDIA, + "AMD" | "ATI Technologies" | "ATI Technologies Inc." => Self::AMD, + _ => Self::OTHER + } + } +} + +#[derive(Debug, Clone, Copy)] +pub enum TreeType { + Fragment, + Vertex, + Geometry, + Compute, +} + +impl From<&str> for TreeType { + fn from(ext: &str) -> Self { + 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!(); + } + } +} diff --git a/server/main/src/opengl.rs b/server/opengl/src/opengl_context.rs similarity index 86% rename from server/main/src/opengl.rs rename to server/opengl/src/opengl_context.rs index 9954e44..4a5a4e9 100644 --- a/server/main/src/opengl.rs +++ b/server/opengl/src/opengl_context.rs @@ -1,24 +1,18 @@ use std::ffi::{CStr, CString}; use std::ptr; -use slog_scope::info; +use glutin::platform::unix::EventLoopExtUnix; +use logging::info; -#[cfg(test)] -use mockall::automock; +use crate::ShaderValidator; -#[cfg_attr(test, automock)] -pub trait ShaderValidator { - fn validate(&self, tree_type: super::TreeType, source: &str) -> Option; - fn vendor(&self) -> String; -} - -pub struct OpenGlContext { +pub(crate) struct Context { _ctx: glutin::Context, } -impl OpenGlContext { - pub fn new() -> OpenGlContext { - let events_loop = glutin::event_loop::EventLoop::new(); +impl Context { + pub fn default() -> Context { + let events_loop = glutin::event_loop::EventLoop::<()>::new_any_thread(); let gl_window = glutin::ContextBuilder::new() .build_headless(&*events_loop, glutin::dpi::PhysicalSize::new(1, 1)) .unwrap(); @@ -29,7 +23,7 @@ impl OpenGlContext { gl_window }; - let gl_ctx = OpenGlContext { _ctx: gl_window }; + let gl_ctx = Context { _ctx: gl_window }; unsafe { info!( @@ -62,15 +56,13 @@ impl OpenGlContext { ); info.set_len((info_len - 1) as usize); // ignore null for str::from_utf8 Some(String::from_utf8(info).unwrap()) - } else { - None - }; + } else { None }; gl::DeleteShader(shader); result } } -impl ShaderValidator for OpenGlContext { +impl ShaderValidator for Context { fn validate(&self, tree_type: super::TreeType, source: &str) -> Option { unsafe { match tree_type { diff --git a/server/opengl/src/opengl_context_facade.rs b/server/opengl/src/opengl_context_facade.rs new file mode 100644 index 0000000..e7f335d --- /dev/null +++ b/server/opengl/src/opengl_context_facade.rs @@ -0,0 +1,87 @@ +use std::{ + sync::{ + mpsc::{self, Receiver, SyncSender}, + Arc, + }, + thread, +}; + +use crate::{Context, ShaderValidator}; + +enum ClientMessage { + Validate { tree_type: crate::TreeType, source: Arc }, + Vendor, +} + +enum ServerMessage { + Validate(Option), + Vendor(String), +} + +/// +pub struct ContextFacade { + start_chan: SyncSender<()>, + client_tx: SyncSender, + server_rx: Receiver, +} + +impl ContextFacade { + pub fn default() -> Self { + let (client_tx, client_rx) = mpsc::sync_channel::(1); + let (server_tx, server_rx) = mpsc::sync_channel::(1); + let (start_chan, start_chan_recv) = mpsc::sync_channel::<()>(1); + thread::spawn(move || { + start_chan_recv.recv().unwrap(); + + let opengl_ctx = Context::default(); + loop { + match client_rx.recv() { + Ok(msg) => { + if let ClientMessage::Validate { tree_type, source } = msg { + server_tx + .send(ServerMessage::Validate(opengl_ctx.validate(tree_type, &source))) + .unwrap(); + } else { + server_tx.send(ServerMessage::Vendor(opengl_ctx.vendor())).unwrap(); + } + } + Err(_) => return, + } + start_chan_recv.recv().unwrap(); + } + }); + + ContextFacade { + start_chan, + client_tx, + server_rx, + } + } +} + +impl ShaderValidator for ContextFacade { + fn validate(&self, tree_type: crate::TreeType, source: &str) -> Option { + self.start_chan.send(()).unwrap(); + match self.client_tx.send(ClientMessage::Validate { + tree_type, + source: Arc::from(source), + }) { + Ok(_) => match self.server_rx.recv().unwrap() { + ServerMessage::Validate(output) => output, + ServerMessage::Vendor(_) => panic!("expected validate reply, got vendor"), + }, + Err(e) => panic!("error sending vendor message: {:?}", e), + } + } + + fn vendor(&self) -> String { + self.start_chan.send(()).unwrap(); + match self.client_tx.send(ClientMessage::Vendor) { + Ok(_) => match self.server_rx.recv().unwrap() { + ServerMessage::Validate(_) => panic!("expected vendor reply, got validate"), + ServerMessage::Vendor(resp) => resp, + }, + Err(e) => panic!("error sending vendor message: {:?}", e), + } + } +} diff --git a/server/server/Cargo.toml b/server/server/Cargo.toml new file mode 100644 index 0000000..eb8f0e4 --- /dev/null +++ b/server/server/Cargo.toml @@ -0,0 +1,49 @@ +[package] +name = "server" +version = "0.9.8" +authors = ["Noah Santschi-Cooney "] +edition = "2021" + +[lib] +doctest = false + +[dependencies] +serde_json = "1.0" +serde = "1.0" + +walkdir = "2.3" +graph = { path = "../graph" } +lazy_static = "1.4" +regex = "1.4" +url = "2.2" +mockall = "0.11" +path-slash = "0.1" +glob = "0.3" +filesystem = { path = "../filesystem" } + +# glutin = "0.28" +gl = "0.14" + +anyhow = "1.0" +thiserror = "1.0" + +tree-sitter = "0.20" +tree-sitter-glsl = "0.1" +logging = { path = "../logging" } +logging_macro = { path = "../logging_macro" } + +tower-lsp = "0.17" +# tower-lsp = { path = "../../../Rust/tower-lsp" } +tokio = { version = "1.18", features = ["full"] } +futures = "0.3" + +workspace = { path = "../workspace" } +opengl = { path = "../opengl" } +sourcefile = { path = "../sourcefile" } + +[dev-dependencies] +tempdir = "0.3" +fs_extra = "1.2" +hamcrest2 = "*" +pretty_assertions = "1.1" +tower-test = "0.4" \ No newline at end of file diff --git a/server/server/src/commands/graph_dot.rs b/server/server/src/commands/graph_dot.rs new file mode 100644 index 0000000..6c7732f --- /dev/null +++ b/server/server/src/commands/graph_dot.rs @@ -0,0 +1,37 @@ +use std::fs::OpenOptions; +use std::io::prelude::*; + +use filesystem::NormalizedPathBuf; +use graph::{CachedStableGraph, Config, dot}; +use logging::info; +// use opengl::IncludePosition; +// use serde_json::Value; + +// use anyhow::{format_err, Result}; + +// pub(crate) fn run(root: &NormalizedPathBuf, graph: &CachedStableGraph) -> Result> { +// let filepath = root.join("graph.dot"); + +// info!("generating dot file"; "path" => &filepath); + +// let mut file = OpenOptions::new().truncate(true).write(true).create(true).open(filepath).unwrap(); + +// let mut write_data_closure = || -> Result<(), std::io::Error> { +// 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.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(None), +// } +// } diff --git a/server/server/src/commands/merged_includes.rs b/server/server/src/commands/merged_includes.rs new file mode 100644 index 0000000..bd67712 --- /dev/null +++ b/server/server/src/commands/merged_includes.rs @@ -0,0 +1,42 @@ +use std::collections::{hash_map::Entry, HashMap}; + +use filesystem::{LFString, NormalizedPathBuf}; +use graph::{dfs, CachedStableGraph}; +use logging::{logger, FutureExt}; +// use opengl::{merge_views, source_mapper::SourceMapper, IncludePosition}; +// use serde_json::Value; + +// use anyhow::{format_err, Result}; + +// pub async fn run(path: &NormalizedPathBuf, graph: &mut CachedStableGraph) -> Result> { +// if graph.root_ancestors_for_key(path)?.is_none() { +// return Err(format_err!("'{}' is not a top-level file aka has ancestors", path)); +// }; + +// //info!("ancestors for {}:\n\t{:?}", path, file_ancestors.iter().map(|e| graph.borrow().graph.node_weight(*e).unwrap().clone()).collect::>()); + +// // if we are a top-level file (this has to be one of the set defined by Optifine, right?) +// // gather the list of all descendants +// let root = graph.find_node(path).unwrap(); + +// let mut sources = HashMap::new(); + +// let tree = dfs::Dfs::new(graph, root) +// .map(|result| { +// let node = result?; +// let path = &graph[node.child]; +// if let Entry::Vacant(entry) = sources.entry(path.clone()) { +// let source = futures::executor::block_on(async { LFString::read(path).with_logger(logger()).await })?; +// entry.insert(source); +// }; +// Ok(node) +// }) +// .collect::>>()?; + +// let mut source_mapper = SourceMapper::new(sources.len()); +// let view = merge_views::MergeViewBuilder::new(&tree, &sources, graph, &mut source_mapper).build(); + +// eprintln!("{:?}", view); + +// Ok(Some(serde_json::value::Value::String(view.to_string()))) +// } diff --git a/server/server/src/commands/mod.rs b/server/server/src/commands/mod.rs new file mode 100644 index 0000000..afd7c62 --- /dev/null +++ b/server/server/src/commands/mod.rs @@ -0,0 +1,3 @@ +pub mod graph_dot; +pub mod merged_includes; +pub mod parse_tree; \ No newline at end of file diff --git a/server/main/src/commands/parse_tree.rs b/server/server/src/commands/parse_tree.rs similarity index 57% rename from server/main/src/commands/parse_tree.rs rename to server/server/src/commands/parse_tree.rs index fb3d9e7..a1de6e4 100644 --- a/server/main/src/commands/parse_tree.rs +++ b/server/server/src/commands/parse_tree.rs @@ -1,42 +1,26 @@ -use std::{ - cell::RefCell, - fs, - path::{Path, PathBuf}, - rc::Rc, -}; +use std::fs; use anyhow::{format_err, Result}; +use filesystem::NormalizedPathBuf; +use logging::warn; use serde_json::Value; -use slog_scope::warn; use tree_sitter::{Parser, TreeCursor}; -use crate::url_norm::FromJson; +fn run_command(path: &NormalizedPathBuf, tree_sitter: &mut Parser) -> Result { + warn!("parsing"; "path" => path); -use super::Invokeable; + let source = fs::read_to_string(path)?; -pub struct TreeSitterSExpr { - pub tree_sitter: Rc>, -} + let tree = match tree_sitter.parse(source, None) { + Some(tree) => tree, + None => return Err(format_err!("tree-sitter parsing resulted in no parse tree")), + }; -impl Invokeable for TreeSitterSExpr { - fn run_command(&self, _: &Path, arguments: &[Value]) -> Result { - let path = PathBuf::from_json(arguments.get(0).unwrap())?; + let mut cursor = tree.walk(); - warn!("parsing"; "path" => path.to_str().unwrap().to_string()); + let rendered = render_parse_tree(&mut cursor); - 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)) - } + Ok(serde_json::value::Value::String(rendered)) } fn render_parse_tree(cursor: &mut TreeCursor) -> String { @@ -76,8 +60,11 @@ fn render_parse_tree(cursor: &mut TreeCursor) -> String { }; string += (" ".repeat(indent) - + format!("{}{} [{}, {}] - [{}, {}]\n", field_name, display_name, start.row, start.column, end.row, end.column) - .trim_start()) + + format!( + "{}{} [{}, {}] - [{}, {}]\n", + field_name, display_name, start.row, start.column, end.row, end.column + ) + .trim_start()) .as_str(); } diff --git a/server/server/src/lib.rs b/server/server/src/lib.rs new file mode 100644 index 0000000..a1cc54d --- /dev/null +++ b/server/server/src/lib.rs @@ -0,0 +1,5 @@ +#![feature(result_option_inspect)] + +pub mod server; +pub use server::*; +mod commands; diff --git a/server/server/src/server.rs b/server/server/src/server.rs new file mode 100644 index 0000000..9434cf0 --- /dev/null +++ b/server/server/src/server.rs @@ -0,0 +1,880 @@ +use std::{collections::HashMap, marker::Sync, sync::Arc}; + +use filesystem::NormalizedPathBuf; +// use futures::future::join_all; +use logging::{error, info, logger, trace, warn, FutureExt}; +use serde_json::Value; + +use tokio::sync::Mutex; + +// #[cfg(test)] +// use test::Client; +// #[cfg(not(test))] +use tower_lsp::Client; + +use tower_lsp::{ + jsonrpc::{Error, ErrorCode, Result}, + lsp_types::{ + notification::{ShowMessage, TelemetryEvent}, + *, + }, + LanguageServer, +}; + +use workspace::WorkspaceManager; + +// use crate::commands; + +pub struct Server +where + G: opengl::ShaderValidator + Send, + F: Fn() -> G, +{ + pub client: Arc>, + workspace_manager: Arc>>, +} + +impl Server +where + G: opengl::ShaderValidator + Send, + F: Fn() -> G, +{ + pub fn new(client: Client, gl_factory: F) -> Self { + Server { + client: Arc::new(Mutex::new(client)), + workspace_manager: Arc::new(Mutex::new(WorkspaceManager::new(gl_factory))), + } + } + + fn capabilities() -> ServerCapabilities { + ServerCapabilities { + definition_provider: Some(OneOf::Left(false)), + references_provider: Some(OneOf::Left(false)), + document_symbol_provider: Some(OneOf::Left(false)), + document_link_provider: /* Some(DocumentLinkOptions { + resolve_provider: None, + work_done_progress_options: WorkDoneProgressOptions { work_done_progress: None }, + }), */ + None, + execute_command_provider: Some(ExecuteCommandOptions { + commands: vec!["graphDot".into(), "virtualMerge".into(), "parseTree".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) })), + })), + workspace: Some(WorkspaceServerCapabilities { + workspace_folders: Some(WorkspaceFoldersServerCapabilities{ + supported: Some(true), + change_notifications: Some(OneOf::Left(false)), + }), + file_operations: None, + }), + semantic_tokens_provider: Some( + SemanticTokensOptions { + work_done_progress_options: WorkDoneProgressOptions { work_done_progress: None }, + legend: SemanticTokensLegend { + token_types: vec![SemanticTokenType::COMMENT], + token_modifiers: vec![], + }, + range: None, + full: Some(SemanticTokensFullOptions::Bool(true)), + } + .into(), + ), + ..ServerCapabilities::default() + } + } + + async fn publish_diagnostic(&self, diagnostics: HashMap>, document_version: Option) { + let client = self.client.lock().with_logger(logger()).await; + // let mut handles = Vec::with_capacity(diagnostics.len()); + for (url, diags) in diagnostics { + eprintln!("publishing to {:?} {:?}", &url, diags); + /* handles.push( */ + client.publish_diagnostics(url, diags, document_version).with_logger(logger()).await; + client + .log_message(MessageType::INFO, "PUBLISHING!") + .with_logger(logger()) + .await; + // client.send_notification::(PublishDiagnosticsParams { + // ri: url, + // diagnostics: diags, + // // version: document_version, + // version: None, + // }).await/* ) */; + } + // join_all(handles).with_logger(logger()).await; + eprintln!("published") + } +} + +#[tower_lsp::async_trait] +impl LanguageServer for Server +where + G: opengl::ShaderValidator + Send, + F: Fn() -> G + Send + Sync, +{ + #[logging::with_trace_id] + async fn initialize(&self, params: InitializeParams) -> Result { + info!("starting server..."); + + let capabilities = Server::::capabilities(); + + let root: NormalizedPathBuf = match params.root_uri { + Some(uri) => uri.into(), + None => { + return Err(Error { + code: ErrorCode::InvalidParams, + message: "Must be in workspace".into(), + data: Some(serde_json::to_value(InitializeError { retry: false }).unwrap()), + }); + } + }; + + let mut manager = self.workspace_manager.lock().with_logger(logger()).await; + + // self.client + // .lock() + // .with_logger(logger()) + // .await + // .send_notification::(serde_json::json!({ + // "status": "loading", + // "message": "Building dependency graph...", + // "icon": "$(loading~spin)", + // })) + // .with_logger(logger()) + // .await; + + manager.gather_workspaces(&root).with_logger(logger()).await; + + // self.client + // .lock() + // .with_logger(logger()) + // .await + // .send_notification::(serde_json::json!({ + // "status": "ready", + // "message": "Project(s) initialized...", + // "icon": "$(check)", + // })) + // .with_logger(logger()) + // .await; + + Ok(InitializeResult { + capabilities, + server_info: None, + }) + } + + async fn initialized(&self, _: InitializedParams) { + // self.client + // .lock() + // .with_logger(logger()) + // .await + // .log_message(MessageType::INFO, "command executed!") + // .with_logger(logger()) + // .await; + } + + async fn shutdown(&self) -> Result<()> { + warn!("shutting down language server..."); + Ok(()) + } + + #[logging::with_trace_id] + async fn did_open(&self, params: DidOpenTextDocumentParams) { + self.client + .lock() + .with_logger(logger()) + .await + .log_message(MessageType::INFO, "OPENED!") + .with_logger(logger()) + .await; + self.client + .lock() + .with_logger(logger()) + .await + .send_notification::(serde_json::json!({ + "status": "ready", + "message": "Project(s) initialized...", + "icon": "$(check)", + })) + .with_logger(logger()) + .await; + info!("opened document"; "uri" => params.text_document.uri.as_str()); + + let path: NormalizedPathBuf = params.text_document.uri.into(); + if let Some(workspace) = self + .workspace_manager + .lock() + .with_logger(logger()) + .await + .find_workspace_for_file(&path) + { + trace!("found workspace"; "root" => &workspace.root); + + workspace.refresh_graph_for_file(&path).with_logger(logger()).await; + + match workspace.lint(&path).with_logger(logger()).await { + Ok(diagnostics) => self.publish_diagnostic(diagnostics, None).with_logger(logger()).await, + Err(e) => error!("error linting"; "error" => format!("{:?}", e), "path" => &path), + } + } + } + + #[logging::with_trace_id] + async fn did_save(&self, params: DidSaveTextDocumentParams) { + info!("saved document"; "uri" => params.text_document.uri.as_str()); + + let path: NormalizedPathBuf = params.text_document.uri.into(); + match self + .workspace_manager + .lock() + .with_logger(logger()) + .await + .find_workspace_for_file(&path) + { + Some(workspace) => { + trace!("found workspace"; "root" => &workspace.root); + + workspace.refresh_graph_for_file(&path).with_logger(logger()).await; + + match workspace.lint(&path).with_logger(logger()).await { + Ok(diagnostics) => self.publish_diagnostic(diagnostics, None).with_logger(logger()).await, + Err(e) => error!("error linting"; "error" => format!("{:?}", e), "path" => &path), + } + } + None => warn!("no workspace found"; "path" => path), + } + } + + #[logging::with_trace_id] + async fn execute_command(&self, params: ExecuteCommandParams) -> Result> { + match params.command.as_str() { + // "graphDot" => { + // let document_path: NormalizedPathBuf = params.arguments.first().unwrap().try_into().unwrap(); + // let manager = self.workspace_manager.lock().with_logger(logger()).await; + // let workspace = manager.find_workspace_for_file(&document_path).unwrap(); + // let graph = workspace.graph.lock().with_logger(logger()).await; + // commands::graph_dot::run(&workspace.root, &graph).map_err(|e| Error { + // code: ErrorCode::InternalError, + // message: format!("{:?}", e), + // data: None, + // }) + // } + // "virtualMerge" => { + // let document_path: NormalizedPathBuf = params.arguments.first().unwrap().try_into().unwrap(); + // let manager = self.workspace_manager.lock().with_logger(logger()).await; + // let workspace = manager.find_workspace_for_file(&document_path).unwrap(); + // let mut graph = workspace.graph.lock().with_logger(logger()).await; + // commands::merged_includes::run(&document_path, &mut graph) + // .with_logger(logger()) + // .await + // .map_err(|e| Error { + // code: ErrorCode::InternalError, + // message: format!("{:?}", e), + // data: None, + // }) + // } + // "parseTree", + _ => Err(Error { + code: ErrorCode::InternalError, + message: "command doesn't exist".into(), + data: None, + }), + } + .inspect_err(|e| { + futures::executor::block_on(async { + self.client + .lock() + .with_logger(logger()) + .await + .send_notification::(ShowMessageParams { + typ: MessageType::ERROR, + message: format!("Failed to execute `{}`: {}.", params.command, e), + }) + .with_logger(logger()) + .await; + }); + }) + .inspect(|_| { + futures::executor::block_on(async { + self.client + .lock() + .with_logger(logger()) + .await + .send_notification::(ShowMessageParams { + typ: MessageType::INFO, + message: format!("Command `{}` executed successfully.", params.command), + }) + .with_logger(logger()) + .await; + }); + }) + } + + async fn goto_definition(&self, _params: GotoDefinitionParams) -> Result> { + /* 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: (), + })), + } + }); + } */ + Ok(None) + } + + async fn references(&self, _params: ReferenceParams) -> Result>> { + /* 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: (), + })), + } + }); */ + Ok(None) + } + + async fn document_symbol(&self, _params: DocumentSymbolParams) -> Result> { + /* 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_document_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: (), + })) + } + } + }); */ + Ok(None) + } + + async fn document_link(&self, _params: DocumentLinkParams) -> Result>> { + /* 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 = self + .graph + .borrow() + .child_node_indexes(node) + .filter_map::, _>(|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::>()), + "path" => curr_doc.to_str().unwrap(), + ); + completable.complete(Ok(edges)); + }); */ + Ok(None) + } + + async fn did_change_configuration(&self, _params: DidChangeConfigurationParams) { + eprintln!("got notif"); + /* logging::slog_with_trace_id(|| { + #[derive(Deserialize)] + struct Configuration { + #[serde(alias = "logLevel")] + log_level: String, + } + + let config: Configuration = from_value(params.settings.as_object().unwrap().get("mcglsl").unwrap().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)); + }) + }); */ + } +} + +#[allow(unused)] +#[cfg(test)] +mod test { + use std::collections::HashSet; + use std::fs; + use std::path::Path; + use std::path::PathBuf; + use std::sync::Arc; + + use filesystem::NormalizedPathBuf; + use logging::warn; + use opengl::MockShaderValidator; + use pretty_assertions::assert_eq; + + use serde_json::json; + use serde_json::Value; + use tempdir::TempDir; + + use fs_extra::{copy_items, dir}; + + use tower_lsp::lsp_types::Diagnostic; + use tower_lsp::ClientSocket; + use tower_lsp::LanguageServer; + use tower_lsp::LspService; + use tower_test::mock::Spawn; + use url::Url; + + use crate::server::Server; + + // implements a noop client for testing sake + pub struct Client; + + impl Client { + pub async fn send_notification(&self, _: N::Params) + where + N: tower_lsp::lsp_types::notification::Notification, + { + } + + pub async fn publish_diagnostics(&self, uri: Url, diags: Vec, version: Option) {} + } + + pub fn new_temp_server(gl_factory: F) -> Server + where + F: Fn() -> MockShaderValidator + Send + Sync, + { + Server::new(Client {}, gl_factory) + } + + #[macro_export] + macro_rules! assert_exchange { + ($service:expr, $request:expr, $response:expr, $method:path) => { + assert_eq!($method($service, $request).await, $response); + }; + } + + fn copy_to_tmp_dir(test_path: &str) -> (TempDir, PathBuf) { + let tmp_dir = TempDir::new("mcshader").unwrap(); + fs::create_dir(tmp_dir.path().join("shaders")).unwrap(); + + { + let test_path = Path::new(test_path) + .canonicalize() + .unwrap_or_else(|_| panic!("canonicalizing '{}'", test_path)); + let opts = &dir::CopyOptions::new(); + let files = fs::read_dir(&test_path) + .unwrap() + .map(|e| String::from(e.unwrap().path().to_str().unwrap())) + .collect::>(); + copy_items(&files, &tmp_dir.path().join("shaders"), opts).unwrap(); + } + + let tmp_path = tmp_dir.path().to_str().unwrap().into(); + + (tmp_dir, tmp_path) + } + + #[tokio::test] + #[logging_macro::scope] + async fn test_empty_initialize() { + let mut server = new_temp_server(MockShaderValidator::new); + + let tmp_dir = TempDir::new("mcshader").unwrap(); + let tmp_path = tmp_dir.path(); + + let init_req = initialize::request(tmp_path); + let init_resp = Ok(initialize::response()); + assert_exchange!(&server, init_req, init_resp, Server::initialize); + + assert_eq!(server.workspace_manager.lock().await.workspaces().len(), 0); + } + + #[tokio::test(flavor = "multi_thread", worker_threads = 1)] + #[logging_macro::scope] + async fn test_01_initialize() { + let mut server = new_temp_server(MockShaderValidator::new); + + let (_tmp_dir, tmp_path) = copy_to_tmp_dir("../testdata/01"); + + let init_req = initialize::request(&tmp_path); + let init_resp = Ok(initialize::response()); + assert_exchange!(&server, init_req, init_resp, Server::initialize); + + let manager = server.workspace_manager.lock().await; + let workspaces = manager.workspaces(); + assert_eq!( + workspaces.iter().map(|w| w.root.to_string()).collect::>(), + vec![tmp_path.to_str().unwrap()] + ); + + // let workspace = workspaces.first().unwrap(); + // let graph = workspace.graph.lock().await; + // // Assert there is one edge between two nodes + // assert_eq!(graph.inner().edge_count(), 1); + + // let edge = graph.inner().edge_indices().next().unwrap(); + // let (node1, node2) = graph.inner().edge_endpoints(edge).unwrap(); + + // // Assert the values of the two nodes in the tree + // assert_eq!(graph.inner()[node1], tmp_path.join("shaders").join("final.fsh").into()); + // assert_eq!(graph.inner()[node2], tmp_path.join("shaders").join("common.glsl").into()); + + // assert_eq!(graph.inner().edge_weight(edge).unwrap().line, 2); + } + + #[tokio::test(flavor = "multi_thread", worker_threads = 1)] + #[logging_macro::scope] + async fn test_05_initialize() { + let mut server = new_temp_server(MockShaderValidator::new); + + let (_tmp_dir, tmp_path) = copy_to_tmp_dir("../testdata/05"); + + let init_req = initialize::request(&tmp_path); + let init_resp = Ok(initialize::response()); + assert_exchange!(&server, init_req, init_resp, Server::initialize); + + let manager = server.workspace_manager.lock().await; + let workspaces = manager.workspaces(); + assert_eq!( + workspaces.iter().map(|w| w.root.to_string()).collect::>(), + vec![tmp_path.to_str().unwrap()] + ); + + // let workspace = workspaces.first().unwrap(); + // let graph = workspace.graph.lock().await; + + // // Assert there is one edge between two nodes + // assert_eq!(graph.inner().edge_count(), 3); + + // assert_eq!(graph.inner().node_count(), 4); + + // let pairs: HashSet<(NormalizedPathBuf, NormalizedPathBuf)> = vec![ + // ( + // tmp_path.join("shaders").join("final.fsh").into(), + // tmp_path.join("shaders").join("common.glsl").into(), + // ), + // ( + // tmp_path.join("shaders").join("final.fsh").into(), + // tmp_path.join("shaders").join("test").join("banana.glsl").into(), + // ), + // ( + // tmp_path.join("shaders").join("test").join("banana.glsl").into(), + // tmp_path.join("shaders").join("test").join("burger.glsl").into(), + // ), + // ] + // .into_iter() + // .collect(); + + // for edge in graph.inner().edge_indices() { + // let endpoints = graph.inner().edge_endpoints(edge).unwrap(); + // let first = &graph[endpoints.0]; + // let second = &graph[endpoints.1]; + // let contains = pairs.contains(&(first.clone(), second.clone())); + // assert!(contains, "doesn't contain ({:?}, {:?})", first, second); + // } + } + + #[macro_export] + macro_rules! from_request { + ($($json:tt)+) => { + { + use tower_lsp::jsonrpc; + use serde_json::{json, Value}; + serde_json::from_value::(json!($($json)+)) + .map(|msg| msg.params().unwrap().clone()) + .map(|value| serde_json::from_value(value)) + .unwrap() + .unwrap() + } + }; + } + + #[macro_export] + macro_rules! from_response { + ($($json:tt)+) => { + { + use tower_lsp::jsonrpc; + use serde_json::{json, Value}; + serde_json::from_value::(json!($($json)+)) + .map(|msg| msg.result().unwrap().clone()) + .map(|value| serde_json::from_value(value)) + .unwrap() + .unwrap() + } + }; + } + + pub mod exit { + use serde_json::{json, Value}; + + pub fn notification() -> Value { + json!({ + "jsonrpc": "2.0", + "method": "exit", + }) + } + } + + pub mod initialize { + use std::path::Path; + + use tower_lsp::{jsonrpc, lsp_types}; + use url::Url; + + pub fn request(root: &Path) -> lsp_types::InitializeParams { + from_request!({ + "jsonrpc": "2.0", + "method": "initialize", + "params": { + "rootUri": Url::from_directory_path(root).unwrap(), + "capabilities":{}, + }, + "id": 1, + }) + } + + pub fn response() -> lsp_types::InitializeResult { + use crate::server::Server; + use opengl::MockShaderValidator; + + from_response!({ + "jsonrpc": "2.0", + "result": { + "capabilities": Server:: MockShaderValidator>::capabilities(), + }, + "id": 1, + }) + } + } + + pub mod initialized { + use tower_lsp::lsp_types; + + pub fn notification() -> lsp_types::InitializedParams { + from_request!({ + "jsonrpc": "2.0", + "method": "initialized", + "params": {}, + }) + } + } + + /* pub mod text_document { + pub mod did_change { + + pub mod notification { + use serde_json::{json, Value}; + use tower_lsp::lsp_types::*; + + pub fn entire>(uri: &Url, text: S) -> Value { + json!({ + "jsonrpc": "2.0", + "method": "textDocument/didChange", + "params": { + "textDocument": { + "uri": uri, + }, + "contentChanges": [ + { + "text": text.as_ref(), + } + ], + }, + }) + } + } + } + + pub mod did_close { + use serde_json::{json, Value}; + use tower_lsp::lsp_types::*; + + pub fn notification(uri: &Url) -> Value { + json!({ + "jsonrpc": "2.0", + "method": "textDocument/didClose", + "params": { + "textDocument": { + "uri": uri, + }, + }, + }) + } + } + + pub mod did_open { + use serde_json::{json, Value}; + use tower_lsp::lsp_types::*; + + pub fn notification, T: AsRef>(uri: &Url, language_id: S, version: i64, text: T) -> Value { + json!({ + "jsonrpc": "2.0", + "method": "textDocument/didOpen", + "params": { + "textDocument": { + "uri": uri, + "languageId": language_id.as_ref(), + "version": version, + "text": text.as_ref(), + }, + }, + }) + } + } + + pub mod document_symbol { + use serde_json::{json, Value}; + use tower_lsp::lsp_types::*; + + pub fn request(uri: &Url) -> Value { + json!({ + "jsonrpc": "2.0", + "method": "textDocument/documentSymbol", + "params": { + "textDocument": { + "uri": uri, + }, + }, + "id": 1, + }) + } + + pub fn response(response: DocumentSymbolResponse) -> Value { + json!({ + "jsonrpc": "2.0", + "result": response, + "id": 1, + }) + } + } + + pub mod hover { + use serde_json::{json, Value}; + use tower_lsp::lsp_types::*; + + pub fn request(uri: &Url, position: Position) -> Value { + json!({ + "jsonrpc": "2.0", + "method": "textDocument/hover", + "params": { + "textDocument": { + "uri": uri, + }, + "position": position, + }, + "id": 1, + }) + } + + pub fn response() -> Value { + json!({ + "jsonrpc": "2.0", + "result": { + }, + "id": 1, + }) + } + } + + pub mod publish_diagnostics { + use serde_json::{json, Value}; + use tower_lsp::lsp_types::*; + + pub fn notification(uri: &Url, diagnostics: &[Diagnostic]) -> Value { + json!({ + "jsonrpc": "2.0", + "method": "textDocument/publishDiagnostics", + "params": { + "uri": uri, + "diagnostics": diagnostics, + }, + }) + } + } + } */ +} diff --git a/server/sourcefile/Cargo.toml b/server/sourcefile/Cargo.toml new file mode 100644 index 0000000..07081ef --- /dev/null +++ b/server/sourcefile/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "sourcefile" +version = "0.9.8" +authors = ["Noah Santschi-Cooney "] +edition = "2021" + +[lib] +doctest = false + +[dependencies] +anyhow = "1.0" + +tower-lsp = "0.17.0" +tokio = { version = "1.18", features = ["fs"]} + +logging = { path = "../logging" } +filesystem = { path = "../filesystem" } + +tree-sitter = "0.20.6" +tree-sitter-glsl = "0.1.2" + +[dev-dependencies] +trim-margin = "0.1" \ No newline at end of file diff --git a/server/sourcefile/src/lib.rs b/server/sourcefile/src/lib.rs new file mode 100644 index 0000000..0a328bc --- /dev/null +++ b/server/sourcefile/src/lib.rs @@ -0,0 +1,75 @@ +#![feature(once_cell)] + +mod linemap; +mod source_file; +mod source_mapper; +use std::fmt::{Debug, Display, Formatter}; + +pub use linemap::*; +use logging::Value; +pub use source_file::*; +pub use source_mapper::*; + +#[derive(Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)] +pub struct IncludeLine(usize); + +impl From for usize { + fn from(n: IncludeLine) -> Self { + n.0 + } +} + +impl From for IncludeLine { + fn from(n: usize) -> Self { + IncludeLine(n) + } +} + +impl std::ops::Add for IncludeLine { + type Output = IncludeLine; + + fn add(self, rhs: usize) -> Self::Output { + IncludeLine(self.0 + rhs) + } +} + +impl PartialEq for IncludeLine { + fn eq(&self, other: &usize) -> bool { + self.0 == *other + } +} + +impl Value for IncludeLine { + fn serialize(&self, record: &logging::Record, key: logging::Key, serializer: &mut dyn logging::Serializer) -> logging::Result { + self.0.serialize(record, key, serializer) + } +} + +impl Debug for IncludeLine { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!(f, "{{line: {}}}", self.0) + } +} + +impl Display for IncludeLine { + fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), std::fmt::Error> { + write!(f, "{{line: {}}}", self.0) + } +} + +#[derive(PartialEq, Eq, Debug, Clone, Copy)] +pub enum Version { + Glsl110 = 110, + Glsl120 = 120, + Glsl130 = 130, + Glsl140 = 140, + Glsl150 = 150, + Glsl330 = 330, + Glsl400 = 400, + Glsl410 = 410, + Glsl420 = 420, + Glsl430 = 430, + Glsl440 = 440, + Glsl450 = 450, + Glsl460 = 460, +} diff --git a/server/main/src/linemap.rs b/server/sourcefile/src/linemap.rs similarity index 94% rename from server/main/src/linemap.rs rename to server/sourcefile/src/linemap.rs index 3995100..c42a3b0 100644 --- a/server/main/src/linemap.rs +++ b/server/sourcefile/src/linemap.rs @@ -1,4 +1,4 @@ -use rust_lsp::lsp_types::Position; +use tower_lsp::lsp_types::Position; pub struct LineMap { positions: Vec, @@ -23,12 +23,12 @@ impl LineMap { #[cfg(test)] mod test { - use rust_lsp::lsp_types::Position; + use tower_lsp::lsp_types::Position; use crate::linemap::LineMap; #[test] - #[logging_macro::log_scope] + #[logging::scope] fn test_linemap() { struct Test { string: &'static str, diff --git a/server/sourcefile/src/source_file.rs b/server/sourcefile/src/source_file.rs new file mode 100644 index 0000000..43d71a4 --- /dev/null +++ b/server/sourcefile/src/source_file.rs @@ -0,0 +1,194 @@ +use std::collections::HashMap; +use core::cell::OnceCell; + +use anyhow::Result; +use filesystem::NormalizedPathBuf; +use tree_sitter::{Parser, Query, QueryCursor, Tree}; +use tree_sitter_glsl::language; + +use crate::{linemap::LineMap, IncludeLine, Version}; + +const GET_VERSION: &str = r#" + (translation_unit + (preproc_call + (preproc_directive) @version_str + (preproc_arg) @version_num)) + (#match? @version_str "\#version") +"#; + +const GET_INCLUDES: &str = r#" + (preproc_include + (string_literal) @include) +"#; + +pub struct SourceFile { + pub source: String, + pub path: NormalizedPathBuf, + root: NormalizedPathBuf, + linemap: OnceCell, + tree: OnceCell, + // TODO: use and implement invalidation + includes: HashMap>, +} + +unsafe impl Send for SourceFile {} +unsafe impl Sync for SourceFile {} + +impl SourceFile { + pub fn new(source: String, path: P, root: R) -> Self + where + P: Into, + R: Into, + { + Self { + source, + path: path.into(), + root: root.into(), + linemap: OnceCell::new(), + tree: OnceCell::new(), + includes: HashMap::new(), + } + } + + pub fn linemap(&self) -> &LineMap { + self.linemap.get_or_init(|| LineMap::new(&self.source)) + } + + pub fn version(&self) -> Result { + let query = Query::new(language(), GET_VERSION)?; + let mut query_cursor = QueryCursor::new(); + + let version_num_match = query_cursor + .captures(&query, self.tree().root_node(), self.source.as_bytes()) + .next() + .unwrap() + .0 + .captures[1]; + + Ok( + match version_num_match + .node + .utf8_text(self.source.as_bytes())? + .trim() + .split(' ') + .next() + .unwrap() + { + "110" => Version::Glsl110, + "120" => Version::Glsl120, + "130" => Version::Glsl130, + "140" => Version::Glsl140, + "150" => Version::Glsl150, + "330" => Version::Glsl330, + "400" => Version::Glsl400, + "410" => Version::Glsl410, + "420" => Version::Glsl420, + "430" => Version::Glsl430, + "440" => Version::Glsl440, + "450" => Version::Glsl450, + "460" => Version::Glsl460, + _ => Version::Glsl110, + }, + ) + } + + pub fn includes(&self) -> Result> { + let query = Query::new(language(), GET_INCLUDES)?; + let mut query_cursor = QueryCursor::new(); + + let mut includes = Vec::new(); + + for (m, _) in query_cursor.captures(&query, self.tree().root_node(), self.source.as_bytes()) { + if m.captures.is_empty() { + continue; + } + + let include = m.captures[0]; + let include_str = { + let mut string = include.node.utf8_text(self.source.as_bytes()).unwrap(); + string = &string[1..string.len() - 1]; + if string.starts_with('/') { + self.root.join("shaders").join(string.strip_prefix('/').unwrap()) + } else { + self.path.parent().unwrap().join(string) + } + }; + + includes.push((include_str, IncludeLine(include.node.start_position().row))); + } + + Ok(includes) + } + + pub fn includes_of_path<'a>(&'a self, child: &'a NormalizedPathBuf) -> Result + '_> { + Ok(self.includes()?.into_iter().filter(move |(p, _)| p == child).map(|(_, l)| l)) + } + + fn tree(&self) -> &Tree { + self.tree.get_or_init(|| { + let mut parser = Parser::new(); + parser.set_language(language()).unwrap(); + parser.parse(&self.source, None).unwrap() + }) + } +} + +#[cfg(test)] +mod test { + use crate::{IncludeLine, SourceFile, Version}; + use anyhow::Result; + use trim_margin::MarginTrimmable; + + #[test] + fn test_versions() { + const SOURCE: &str = r#" + #version 150 core + + void main() {} + "#; + + let source = SourceFile::new(SOURCE.to_string(), "/asdf", "/"); + assert_eq!(source.version().unwrap(), Version::Glsl150); + } + + #[test] + fn test_includes() -> Result<()> { + let source = r#" + |#version 330 + | + |#include "path/to/banana.fsh" + | #include "/path/to/badbanana.gsh" + "# + .trim_margin() + .unwrap(); + + let source = SourceFile::new(source, "/myshader/shaders/world0/asdf.fsh", "/myshader"); + assert_eq!( + source.includes()?, + vec![ + ("/myshader/shaders/world0/path/to/banana.fsh".into(), IncludeLine(2)), + ("/myshader/shaders/path/to/badbanana.gsh".into(), IncludeLine(3)) + ] + ); + Ok(()) + } + + #[test] + fn test_single_includes() -> Result<()> { + let source = r#" + |#version 330 + | + |#include "path/to/banana.fsh" + | #include "/path/to/badbanana.gsh" + "# + .trim_margin() + .unwrap(); + + let source = SourceFile::new(source, "/myshader/shaders/world0/asdf.fsh", "/myshader"); + assert_eq!( + source.includes_of_path(&"/myshader/shaders/world0/path/to/banana.fsh".into())?.collect::>(), + vec![IncludeLine(2)] + ); + Ok(()) + } +} diff --git a/server/main/src/source_mapper.rs b/server/sourcefile/src/source_mapper.rs similarity index 60% rename from server/main/src/source_mapper.rs rename to server/sourcefile/src/source_mapper.rs index efcad0d..8ec536b 100644 --- a/server/main/src/source_mapper.rs +++ b/server/sourcefile/src/source_mapper.rs @@ -1,6 +1,4 @@ -use std::{collections::HashMap, fmt::Display}; - -use petgraph::graph::NodeIndex; +use std::{cmp::Eq, collections::HashMap, fmt::Display, hash::Hash}; #[derive(Clone, Copy, PartialEq, Eq, Hash)] pub struct SourceNum(usize); @@ -17,17 +15,19 @@ impl From for SourceNum { } } -// Maps from a graph node index to a virtual OpenGL -// source number (for when building the merged source view), +// Maps from a key 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 { +pub struct SourceMapper { next: SourceNum, - mapping: HashMap, - reverse_mapping: Vec, + mapping: HashMap, + reverse_mapping: Vec, } -impl SourceMapper { +impl SourceMapper +where + T: Eq + Hash + Clone, +{ pub fn new(capacity: usize) -> Self { SourceMapper { next: SourceNum(0), @@ -36,17 +36,17 @@ impl SourceMapper { } } - pub fn get_num(&mut self, node: NodeIndex) -> SourceNum { - let num = &*self.mapping.entry(node).or_insert_with(|| { + pub fn get_num(&mut self, node: &T) -> SourceNum { + let num = &*self.mapping.entry(node.clone()).or_insert_with(|| { let next = self.next; self.next.0 += 1; - self.reverse_mapping.push(node); + self.reverse_mapping.push(node.clone()); next }); *num } - pub fn get_node(&self, num: SourceNum) -> NodeIndex { - self.reverse_mapping[num.0] + pub fn get_node(&self, num: SourceNum) -> &T { + &self.reverse_mapping[num.0] } } diff --git a/server/main/testdata/01/common.glsl b/server/testdata/01/common.glsl similarity index 100% rename from server/main/testdata/01/common.glsl rename to server/testdata/01/common.glsl diff --git a/server/main/testdata/01/final.fsh b/server/testdata/01/final.fsh similarity index 100% rename from server/main/testdata/01/final.fsh rename to server/testdata/01/final.fsh diff --git a/server/testdata/01/final.fsh.merge b/server/testdata/01/final.fsh.merge new file mode 100644 index 0000000..88e8c10 --- /dev/null +++ b/server/testdata/01/final.fsh.merge @@ -0,0 +1,25 @@ +#version 120 +#define MC_VERSION 11800 +#define MC_GL_VERSION 320 +#define MC_GLSL_VERSION 150 +#define MC_OS_LINUX +#define MC_GL_VENDOR_NVIDIA +#define MC_GL_RENDERER_GEFORCE +#define MC_NORMAL_MAP +#define MC_SPECULAR_MAP +#define MC_RENDER_QUALITY 1.0 +#define MC_SHADOW_QUALITY 1.0 +#define MC_HAND_DEPTH 0.125 +#define MC_OLD_HAND_LIGHT +#define MC_OLD_LIGHTING +#line 1 0 // !! + +#line 0 1 // !! +float test() { + return 0.5; +} +#line 3 0 // !! + +void main() { + gl_FragColor[0] = vec4(0.0); +} \ No newline at end of file diff --git a/server/testdata/01/shaders.properties b/server/testdata/01/shaders.properties new file mode 100644 index 0000000..e69de29 diff --git a/server/main/testdata/02/final.fsh b/server/testdata/02/final.fsh similarity index 100% rename from server/main/testdata/02/final.fsh rename to server/testdata/02/final.fsh diff --git a/server/testdata/02/final.fsh.merge b/server/testdata/02/final.fsh.merge new file mode 100644 index 0000000..42ef6d5 --- /dev/null +++ b/server/testdata/02/final.fsh.merge @@ -0,0 +1,41 @@ +#version 120 +#define MC_VERSION 11800 +#define MC_GL_VERSION 320 +#define MC_GLSL_VERSION 150 +#define MC_OS_LINUX +#define MC_GL_VENDOR_NVIDIA +#define MC_GL_RENDERER_GEFORCE +#define MC_NORMAL_MAP +#define MC_SPECULAR_MAP +#define MC_RENDER_QUALITY 1.0 +#define MC_SHADOW_QUALITY 1.0 +#define MC_HAND_DEPTH 0.125 +#define MC_OLD_HAND_LIGHT +#define MC_OLD_LIGHTING +#line 1 0 // !! + +#line 0 1 // !! +int sample() { + return 5; +} + +#line 0 2 // !! +void burger() { + // sample text +} +#line 5 1 // !! + +#line 0 3 // !! +float test() { + return 3.0; +} +#line 7 1 // !! + +int sample_more() { + return 5; +} +#line 3 0 // !! + +void main() { + gl_FragColor = vec4(1.0, 1.0, 1.0, 1.0); +} diff --git a/server/testdata/02/shaders.properties b/server/testdata/02/shaders.properties new file mode 100644 index 0000000..e69de29 diff --git a/server/main/testdata/02/utils/burger.glsl b/server/testdata/02/utils/burger.glsl similarity index 100% rename from server/main/testdata/02/utils/burger.glsl rename to server/testdata/02/utils/burger.glsl diff --git a/server/main/testdata/02/utils/sample.glsl b/server/testdata/02/utils/sample.glsl similarity index 100% rename from server/main/testdata/02/utils/sample.glsl rename to server/testdata/02/utils/sample.glsl diff --git a/server/main/testdata/02/utils/test.glsl b/server/testdata/02/utils/test.glsl similarity index 100% rename from server/main/testdata/02/utils/test.glsl rename to server/testdata/02/utils/test.glsl diff --git a/server/main/testdata/03/final.fsh b/server/testdata/03/final.fsh similarity index 100% rename from server/main/testdata/03/final.fsh rename to server/testdata/03/final.fsh diff --git a/server/testdata/03/final.fsh.merge b/server/testdata/03/final.fsh.merge new file mode 100644 index 0000000..868876d --- /dev/null +++ b/server/testdata/03/final.fsh.merge @@ -0,0 +1,37 @@ +#version 120 +#define MC_VERSION 11800 +#define MC_GL_VERSION 320 +#define MC_GLSL_VERSION 150 +#define MC_OS_LINUX +#define MC_GL_VENDOR_NVIDIA +#define MC_GL_RENDERER_GEFORCE +#define MC_NORMAL_MAP +#define MC_SPECULAR_MAP +#define MC_RENDER_QUALITY 1.0 +#define MC_SHADOW_QUALITY 1.0 +#define MC_HAND_DEPTH 0.125 +#define MC_OLD_HAND_LIGHT +#define MC_OLD_LIGHTING +#line 1 0 // !! + +#line 0 1 // !! +int sample() { + return 5; +} + +#line 0 2 // !! +void burger() { + // sample text +} +#line 5 1 // !! + +#line 0 3 // !! +float test() { + return 3.0; +} +#line 7 1 // !! +#line 3 0 // !! + +void main() { + gl_FragColor = vec4(1.0, 1.0, 1.0, 1.0); +} diff --git a/server/testdata/03/shaders.properties b/server/testdata/03/shaders.properties new file mode 100644 index 0000000..e69de29 diff --git a/server/main/testdata/03/utils/burger.glsl b/server/testdata/03/utils/burger.glsl similarity index 100% rename from server/main/testdata/03/utils/burger.glsl rename to server/testdata/03/utils/burger.glsl diff --git a/server/main/testdata/03/utils/sample.glsl b/server/testdata/03/utils/sample.glsl similarity index 100% rename from server/main/testdata/03/utils/sample.glsl rename to server/testdata/03/utils/sample.glsl diff --git a/server/main/testdata/03/utils/test.glsl b/server/testdata/03/utils/test.glsl similarity index 100% rename from server/main/testdata/03/utils/test.glsl rename to server/testdata/03/utils/test.glsl diff --git a/server/main/testdata/04/final.fsh b/server/testdata/04/final.fsh similarity index 66% rename from server/main/testdata/04/final.fsh rename to server/testdata/04/final.fsh index b45ba1b..9ae8a02 100644 --- a/server/main/testdata/04/final.fsh +++ b/server/testdata/04/final.fsh @@ -1,7 +1,7 @@ #version 120 #include "/utils/utilities.glsl" -#include "/utils/matricies.glsl" +#include "/lib/matrices.glsl" void main() { diff --git a/server/testdata/04/final.fsh.merge b/server/testdata/04/final.fsh.merge new file mode 100644 index 0000000..30fe271 --- /dev/null +++ b/server/testdata/04/final.fsh.merge @@ -0,0 +1,37 @@ +#version 120 +#define MC_VERSION 11800 +#define MC_GL_VERSION 320 +#define MC_GLSL_VERSION 150 +#define MC_OS_LINUX +#define MC_GL_VENDOR_NVIDIA +#define MC_GL_RENDERER_GEFORCE +#define MC_NORMAL_MAP +#define MC_SPECULAR_MAP +#define MC_RENDER_QUALITY 1.0 +#define MC_SHADOW_QUALITY 1.0 +#define MC_HAND_DEPTH 0.125 +#define MC_OLD_HAND_LIGHT +#define MC_OLD_LIGHTING +#line 1 0 // !! + +#line 0 1 // !! +#line 0 2 // !! +void stuff1() { + +} +#line 1 1 // !! +#line 0 3 // !! +void stuff2() { + +} +#line 2 1 // !! +#line 3 0 // !! +#line 0 4 // !! +void matrix() { + +} +#line 4 0 // !! + +void main() { + +} \ No newline at end of file diff --git a/server/main/testdata/04/lib/matrices.glsl b/server/testdata/04/lib/matrices.glsl similarity index 100% rename from server/main/testdata/04/lib/matrices.glsl rename to server/testdata/04/lib/matrices.glsl diff --git a/server/testdata/04/shaders.properties b/server/testdata/04/shaders.properties new file mode 100644 index 0000000..e69de29 diff --git a/server/main/testdata/04/utils/stuff1.glsl b/server/testdata/04/utils/stuff1.glsl similarity index 100% rename from server/main/testdata/04/utils/stuff1.glsl rename to server/testdata/04/utils/stuff1.glsl diff --git a/server/main/testdata/04/utils/stuff2.glsl b/server/testdata/04/utils/stuff2.glsl similarity index 100% rename from server/main/testdata/04/utils/stuff2.glsl rename to server/testdata/04/utils/stuff2.glsl diff --git a/server/main/testdata/04/utils/utilities.glsl b/server/testdata/04/utils/utilities.glsl similarity index 100% rename from server/main/testdata/04/utils/utilities.glsl rename to server/testdata/04/utils/utilities.glsl diff --git a/server/main/testdata/05/common.glsl b/server/testdata/05/common.glsl similarity index 100% rename from server/main/testdata/05/common.glsl rename to server/testdata/05/common.glsl diff --git a/server/main/testdata/05/final.fsh b/server/testdata/05/final.fsh similarity index 100% rename from server/main/testdata/05/final.fsh rename to server/testdata/05/final.fsh diff --git a/server/main/testdata/05/final.fsh.merge b/server/testdata/05/final.fsh.merge similarity index 100% rename from server/main/testdata/05/final.fsh.merge rename to server/testdata/05/final.fsh.merge diff --git a/server/testdata/05/shaders.properties b/server/testdata/05/shaders.properties new file mode 100644 index 0000000..e69de29 diff --git a/server/main/testdata/05/test/banana.glsl b/server/testdata/05/test/banana.glsl similarity index 100% rename from server/main/testdata/05/test/banana.glsl rename to server/testdata/05/test/banana.glsl diff --git a/server/main/testdata/05/test/burger.glsl b/server/testdata/05/test/burger.glsl similarity index 100% rename from server/main/testdata/05/test/burger.glsl rename to server/testdata/05/test/burger.glsl diff --git a/server/main/testdata/06/final.fsh b/server/testdata/06/final.fsh similarity index 100% rename from server/main/testdata/06/final.fsh rename to server/testdata/06/final.fsh diff --git a/server/testdata/06/final.fsh.merge b/server/testdata/06/final.fsh.merge new file mode 100644 index 0000000..65c4824 --- /dev/null +++ b/server/testdata/06/final.fsh.merge @@ -0,0 +1,31 @@ +#version 120 +#define MC_VERSION 11800 +#define MC_GL_VERSION 320 +#define MC_GLSL_VERSION 150 +#define MC_OS_LINUX +#define MC_GL_VENDOR_NVIDIA +#define MC_GL_RENDERER_GEFORCE +#define MC_NORMAL_MAP +#define MC_SPECULAR_MAP +#define MC_RENDER_QUALITY 1.0 +#define MC_SHADOW_QUALITY 1.0 +#define MC_HAND_DEPTH 0.125 +#define MC_OLD_HAND_LIGHT +#define MC_OLD_LIGHTING +#line 1 0 // !! + +#ifdef BANANA +#line 0 1 // !! +int test() { + return 1; +} +#line 4 0 // !! +#else +#line 0 1 // !! +int test() { + return 1; +} +#line 6 0 // !! +#endif + +void main() {} \ No newline at end of file diff --git a/server/testdata/06/shaders.properties b/server/testdata/06/shaders.properties new file mode 100644 index 0000000..e69de29 diff --git a/server/main/testdata/06/test.glsl b/server/testdata/06/test.glsl similarity index 100% rename from server/main/testdata/06/test.glsl rename to server/testdata/06/test.glsl diff --git a/server/workspace/Cargo.toml b/server/workspace/Cargo.toml new file mode 100644 index 0000000..c3ef1c6 --- /dev/null +++ b/server/workspace/Cargo.toml @@ -0,0 +1,29 @@ +[package] +name = "workspace" +version = "0.9.8" +authors = ["Noah Santschi-Cooney "] +edition = "2021" + +[lib] +doctest = false + +[dependencies] +anyhow = "1.0" +filesystem = { path = "../filesystem" } +futures = "0.3.21" +glob = "0.3" +graph = { path = "../graph" } +# include_merger = { path = "../include_merger" } +lazy_static = "1.4" +logging = { path = "../logging" } +opengl = { path = "../opengl" } +path-slash = "0.1" +regex = "1.4" +sourcefile = { path = "../sourcefile" } +tokio = { version = "1.18.0", features = ["sync"] } +tower-lsp = "0.17.0" +tst = "0.10" +url = "2.2" +walkdir = "2.3" +workspace_tree = { path = "../workspace_tree" } +include_merger = { path = "../include_merger" } \ No newline at end of file diff --git a/server/workspace/src/lib.rs b/server/workspace/src/lib.rs new file mode 100644 index 0000000..42465af --- /dev/null +++ b/server/workspace/src/lib.rs @@ -0,0 +1,6 @@ +#![feature(assert_matches)] + +pub mod workspace; +pub mod workspace_manager; +pub use workspace::*; +pub use workspace_manager::*; diff --git a/server/workspace/src/workspace.rs b/server/workspace/src/workspace.rs new file mode 100644 index 0000000..b29e6aa --- /dev/null +++ b/server/workspace/src/workspace.rs @@ -0,0 +1,185 @@ +use std::{collections::HashMap, sync::Arc}; + +use anyhow::Result; +use filesystem::{LFString, NormalizedPathBuf}; +use graph::{dfs, CachedStableGraph, FilialTuple, NodeIndex}; +use include_merger::MergeViewBuilder; +use logging::{info, logger, warn, FutureExt}; +use opengl::{diagnostics_parser::DiagnosticsParser, TreeType}; +use sourcefile::{IncludeLine, SourceFile, SourceMapper}; +use tokio::sync::Mutex; +use tower_lsp::lsp_types::{Diagnostic, DiagnosticSeverity, Position, Range}; +use url::Url; +use workspace_tree::TreeError; + +pub struct Workspace { + pub root: NormalizedPathBuf, + workspace_view: Arc>, + // graph: Arc>>, + gl_context: Arc>, +} + +impl Workspace { + pub fn new(root: NormalizedPathBuf, gl: S) -> Self { + Workspace { + workspace_view: Arc::new(Mutex::new(workspace_tree::WorkspaceTree::new(&root))), + root, + // graph: Arc::new(Mutex::new(CachedStableGraph::new())), + gl_context: Arc::new(Mutex::new(gl)), + } + } + + pub async fn build(&self) { + info!("initializing workspace"; "root" => &self.root); + + let mut tree = self.workspace_view.lock().with_logger(logger()).await; + tree.build(); + + info!("build graph"; "connected" => tree.num_connected_entries()/* , "disconnected" => tree.num_disconnected_entries() */); + } + + pub async fn refresh_graph_for_file(&self, path: &NormalizedPathBuf) { + let mut tree = self.workspace_view.lock().with_logger(logger()).await; + + tree.update_sourcefile(path); + } + + pub async fn lint(&self, path: &NormalizedPathBuf) -> Result>> { + let mut workspace = self.workspace_view.lock().with_logger(logger()).await; + + // the set of all filepath->content. + let mut all_sources: HashMap = HashMap::new(); + // the set of filepath->list of diagnostics to report + let mut diagnostics: HashMap> = HashMap::new(); + + // we want to backfill the diagnostics map with all linked sources + let back_fill = |all_sources: &HashMap, diagnostics: &mut HashMap>| { + for path in all_sources.keys() { + diagnostics.entry(Url::from_file_path(path).unwrap()).or_default(); + } + }; + + let trees = match workspace.trees_for_entry(path) { + Ok(trees) => trees, + Err(err) => { + match err { + TreeError::NonTopLevel(e) => warn!("got a non-valid toplevel file"; "root_ancestor" => e, "stripped" => e.strip_prefix(&self.root), "path" => path), + e => return Err(e.into()), + } + back_fill(&all_sources, &mut diagnostics); + return Ok(diagnostics); + } + } + .collect::>(); + + for tree in trees { + let mut tree = match tree { + Ok(t) => t.peekable(), + Err(e) => match e { + // dont care, didnt ask, skip + TreeError::NonTopLevel(_) => continue, + e => unreachable!("unexpected error {:?}", e), + }, + }; + // let tree = match tree.and_then(|t| t.collect::, TreeError>>()) { + // Ok(t) => t, + // Err(e) => { + // match e { + // TreeError::NonTopLevel(f) => { + // warn!("got a non-valid toplevel file"; "root_ancestor" => f, "stripped" => f.strip_prefix(&self.root)); + // continue; + // } + // TreeError::FileNotFound(f) => { + // warn!("child not found"; "child" => f); + // continue; + // } + // TreeError::DfsError(e) => { + // diagnostics.insert(Url::from_file_path(path).unwrap(), vec![e.into()]); + // back_fill(&all_sources, &mut diagnostics); // TODO: confirm + // return Ok(diagnostics); + // } + // e => unreachable!("should only yield non-toplevel file error, got {:?}", e), + // }; + // } + // }; + + let tree_size = tree.size_hint().0; + + let mut source_mapper = SourceMapper::new(tree_size); // very rough count + + let root = tree + .peek() + .expect("expected non-zero sized tree") + .as_ref() + .expect("unexpected cycle or not-found node") + .child; + + let (tree_type, document_glsl_version) = ( + root.path.extension().unwrap().into(), + root.version().expect("fatal error parsing file for include version"), + ); + + let mut built_tree = Vec::with_capacity(tree_size); + for entry in tree { + match entry { + Ok(node) => built_tree.push(node), + Err(e) => match e { + TreeError::FileNotFound { + ref importing, + ref missing, + } => diagnostics + .entry(Url::from_file_path(importing).unwrap()) + .or_default() + .push(Diagnostic { + range: Range::new(Position::new(0, 0), Position::new(0, u32::MAX)), + severity: Some(DiagnosticSeverity::WARNING), + source: Some("mcglsl".to_string()), + message: e.to_string(), + ..Diagnostic::default() + }), + TreeError::DfsError(_) => todo!(), + e => unreachable!("unexpected error {:?}", e), + }, + } + } + + let view = MergeViewBuilder::new( + &built_tree, + &mut source_mapper, + self.gl_context.lock().with_logger(logger()).await.vendor().as_str().into(), + document_glsl_version, + ) + .build(); + + let stdout = match self.compile_shader_source(&view, tree_type, path).with_logger(logger()).await { + Some(s) => s, + None => { + back_fill(&all_sources, &mut diagnostics); + return Ok(diagnostics); + } + }; + + diagnostics.extend( + DiagnosticsParser::new(&*self.gl_context.lock().with_logger(logger()).await).parse_diagnostics_output( + stdout, + path, + &source_mapper, + ), + ); + } + + back_fill(&all_sources, &mut diagnostics); + Ok(diagnostics) + } + + async fn compile_shader_source(&self, source: &str, tree_type: TreeType, path: &NormalizedPathBuf) -> Option { + let result = self.gl_context.lock().with_logger(logger()).await.validate(tree_type, source); + match &result { + Some(output) => { + info!("compilation errors reported"; "errors" => format!("`{}`", output.replace('\n', "\\n")), "tree_root" => path) + } + None => info!("compilation reported no errors"; "tree_root" => path), + }; + result + } +} diff --git a/server/workspace/src/workspace_manager.rs b/server/workspace/src/workspace_manager.rs new file mode 100644 index 0000000..ba2acff --- /dev/null +++ b/server/workspace/src/workspace_manager.rs @@ -0,0 +1,137 @@ +use std::{ffi::OsStr, path::Path}; + +use filesystem::NormalizedPathBuf; +use glob::{glob_with, MatchOptions}; +use logging::{info, error, FutureExt, logger}; +use tst::TSTMap; +use walkdir::WalkDir; + +use crate::workspace::Workspace; + +pub struct WorkspaceIndex(usize); + +#[derive(Default)] +pub struct WorkspaceManager +where + G: opengl::ShaderValidator + Send, + F: Fn() -> G +{ + search: TSTMap, + workspaces: Vec>, + gl_factory: F +} + +impl WorkspaceManager +where + G: opengl::ShaderValidator + Send, + F: Fn() -> G +{ + pub fn new(gl_factory: F) -> Self { + WorkspaceManager { + search: Default::default(), + workspaces: Default::default(), + gl_factory + } + } + + pub async fn gather_workspaces(&mut self, root: &NormalizedPathBuf) { + let options = MatchOptions { + case_sensitive: true, + ..MatchOptions::default() + }; + + let glob = root.join("**").join("shaders.properties"); + info!("banana"; "glob" => &glob); + + for entry in glob_with(&glob.to_string(), options).unwrap() { + match entry { + Ok(path) + if path.file_name().and_then(OsStr::to_str) == Some("shaders.properties") + && path.parent().and_then(Path::file_name).and_then(OsStr::to_str) == Some("shaders") => + { + match path.parent().and_then(Path::parent).map(Into::into) { + Some(shader_root) => self.add_workspace(&shader_root).with_logger(logger()).await, + None => todo!(), + } + } + Ok(path) + if path.file_name().and_then(OsStr::to_str) == Some("shaders.properties") + && path + .parent() + .and_then(Path::file_name) + .and_then(OsStr::to_str) + .map_or(false, |f| f.starts_with("world")) + && path + .parent() + .and_then(Path::parent) + .and_then(Path::file_name) + .and_then(OsStr::to_str) + == Some("shaders") => + { + match path.parent().and_then(Path::parent).and_then(Path::parent).map(Into::into) { + Some(shader_root) => self.add_workspace(&shader_root).with_logger(logger()).await, + None => todo!(), + } + } + Ok(path) => { + let path: NormalizedPathBuf = path.into(); + error!("shaders.properties found outside ./shaders or ./worldX dir"; "path" => path) + } + Err(e) => error!("error iterating glob entries"; "error" => format!("{:?}", e)), + } + } + + let glob = root.join("**").join("shaders"); + for entry in glob_with(&glob.to_string(), options).unwrap() { + match entry { + Ok(path) + if !WalkDir::new(path.clone()).into_iter().any(|p| { + p.as_ref() + .ok() + .map(|p| p.file_name()) + .and_then(|f| f.to_str()) + .map_or(false, |f| f == "shaders.properties") + }) => + { + match path.parent().map(Into::into) { + Some(shader_root) => self.add_workspace(&shader_root).with_logger(logger()).await, + None => todo!(), + } + } + Ok(path) => { + let path: NormalizedPathBuf = path.into(); + info!("skipping as already existing"; "path" => path) + } + Err(e) => error!("error iterating glob entries"; "error" => format!("{:?}", e)), + } + } + } + + async fn add_workspace(&mut self, root: &NormalizedPathBuf) { + if !self.search.contains_key(&root.to_string()) { + info!("adding workspace"; "root" => &root); + let opengl_context = (self.gl_factory)(); + let workspace = Workspace::new(root.clone(), opengl_context); + workspace.build().with_logger(logger()).await; + self.workspaces.push(workspace); + self.search.insert(&root.to_string(), WorkspaceIndex(self.workspaces.len() - 1)); + } + } + + pub fn find_workspace_for_file(&self, file: &NormalizedPathBuf) -> Option<&Workspace> { + let file = file.to_string(); + let prefix = self.search.longest_prefix(&file); + if prefix.is_empty() { + return None; + } + + match self.search.get(prefix) { + Some(idx) => self.workspaces.get(idx.0), + None => None, + } + } + + pub fn workspaces(&self) -> &[Workspace] { + &self.workspaces + } +} diff --git a/server/workspace_tree/Cargo.toml b/server/workspace_tree/Cargo.toml new file mode 100644 index 0000000..0b5f8b9 --- /dev/null +++ b/server/workspace_tree/Cargo.toml @@ -0,0 +1,27 @@ +[package] +name = "workspace_tree" +version = "0.9.8" +authors = ["Noah Santschi-Cooney "] +edition = "2021" + +[lib] +doctest = false + +[dependencies] +# include_merger = { path = "../include_merger" } +anyhow = "1.0" +filesystem = { path = "../filesystem" } +futures = "0.3.21" +glob = "0.3" +graph = { path = "../graph" } +lazy_static = "1.4" +logging = { path = "../logging" } +opengl = { path = "../opengl" } +path-slash = "0.1" +regex = "1.4" +sourcefile = { path = "../sourcefile" } +thiserror = "1.0" +tokio = { version = "1.18.0", features = ["sync"] } +tst = "0.10" +url = "2.2" +walkdir = "2.3" \ No newline at end of file diff --git a/server/workspace_tree/src/lib.rs b/server/workspace_tree/src/lib.rs new file mode 100644 index 0000000..8ede04f --- /dev/null +++ b/server/workspace_tree/src/lib.rs @@ -0,0 +1,280 @@ +#![feature(result_flattening)] + +use std::{ + collections::{HashMap, HashSet}, + fs::read_to_string, + sync::Arc, +}; + +use filesystem::{is_top_level, NormalizedPathBuf}; +use graph::{ + dfs::{CycleError, Dfs}, + CachedStableGraph, FilialTuple, NodeIndex, +}; +use logging::{debug, info, warn}; +use sourcefile::{IncludeLine, SourceFile}; +use walkdir::WalkDir; + +mod tree; + +pub struct WorkspaceTree { + root: NormalizedPathBuf, + pub graph: CachedStableGraph, + disconnected: HashSet, + sources: HashMap>, +} + +#[derive(thiserror::Error, Debug)] +pub enum TreeError { + #[error("got a non-valid top-level file")] + NonTopLevel(NormalizedPathBuf), + #[error("file {missing} not found; imported by {importing}.")] + FileNotFound { + importing: NormalizedPathBuf, + missing: NormalizedPathBuf, + }, + #[error(transparent)] + DfsError(#[from] CycleError), + #[error(transparent)] + Other(#[from] anyhow::Error), +} + +impl WorkspaceTree { + pub fn new(root: &NormalizedPathBuf) -> Self { + WorkspaceTree { + root: root.clone(), + graph: CachedStableGraph::new(), + disconnected: HashSet::new(), + sources: HashMap::new(), + } + } + + pub fn num_connected_entries(&self) -> usize { + self.graph.node_count() + } + + // pub fn num_disconnected_entries(&self) -> usize { + // self.disconnected.len() + // } + + /// builds the set of connected and disconnected GLSL files from the root of the + /// workspace. + // TODO: support user-defined additional file extensions. + pub fn build(&mut self) { + let root = self.root.clone(); + + enum GraphEntry { + // represents top-level nodes + TopLevel(SourceFile), + // represents non-top-level nodes + Leaf(SourceFile), + } + + // let mut roots = Vec::new(); + + for entry in WalkDir::new(&root) + .into_iter() + .filter_map(Result::ok) + .filter(|entry| entry.path().is_file()) + .map(|entry| NormalizedPathBuf::from(entry.into_path())) + .filter_map(|path| { + // files not imported anywhere wont be included in the graph, + // this is ok for now. + if !is_top_level(&path.strip_prefix(&root)) { + let ext = path.extension(); + if ext == Some("fsh") || ext == Some("gsh") || ext == Some("vsh") || ext == Some("glsl") || ext == Some("csh") { + return Some(GraphEntry::Leaf(SourceFile::new(read_to_string(&path).ok()?, path, root.clone()))); + } + return None; + } + + Some(GraphEntry::TopLevel(SourceFile::new( + read_to_string(&path).ok()?, + path, + root.clone(), + ))) + }) + { + // 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 + match entry { + GraphEntry::TopLevel(file) => { + let file = Arc::new(file); + eprintln!("TOP LEVEL {}", file.path); + let path = file.path.clone(); + // roots.push(file.clone()); + self.sources.insert(path.clone(), file); + self.update_sourcefile(&path); + } + GraphEntry::Leaf(file) => { + let file = Arc::new(file); + eprintln!("LEAF {}", file.path); + let path = file.path.clone(); + self.sources.insert(path.clone(), file); + self.update_sourcefile(&path); + // self.disconnected.insert(path); + } + }; + } + + // for root in roots { + // self.update_sourcefile(&root.path); + // for include in root.includes().unwrap() { + // // for tree_entry in self.trees_for_entry(&include.0).unwrap() { + // // tree_entry.unwrap(). + // // } + // } + // } + } + + /// Returns the lazy depth first iterators for the possible trees given any node. + /// If it is a top-level node, only a single tree should be instantiated. If not a top-level node, + /// a tree will be instantiated for every top-level root ancestor. + /// + /// Error modes: + /// - Top [`Result`] + /// - The node is not known to the workspace + /// - The node has no ancestors but is not a known valid top-level file + /// - Middle [`Result`] (only for >1 ancestor) + /// - A non-valid top-level ancestor was found + /// - Bottom [`Result`] + /// - A cycle was detected while iterating + /// - A node was not found on the filesystem while synthesizing a Sourcefile instance + pub fn trees_for_entry<'a>( + &'a mut self, entry: &'a NormalizedPathBuf, + ) -> Result< + impl Iterator, TreeError>> + '_, TreeError>> + '_, + TreeError, + > { + let root_ancestors = self.graph.root_ancestors_for_key(entry)?.unwrap_or_default(); + + let mut trees = Vec::with_capacity(root_ancestors.len().max(1)); + + info!("top-level file ancestors found"; + "uri" => entry, + "ancestors" => format!("{:?}", root_ancestors.iter() + .copied() + .map(|e| &self.graph.graph[e]) + .collect::>()) + ); + + let node = self.graph.find_node(entry).unwrap(); + + let transform_cycle_error = |result: Result, CycleError>| result.map_err(TreeError::DfsError); + let node_to_sourcefile = |result: Result, TreeError>| -> Result, TreeError> { + result.and_then(|tup| { + // fatal error case, shouldnt happen + let parent = tup.parent.map(|p| { + let parent_path = &self.graph[p]; + self.sources + .get(parent_path) + .unwrap_or_else(|| panic!("no entry in sources for parent {}", parent_path)) + .as_ref() + }); + + let child_path = &self.graph[tup.child]; + // soft-fail case, if file doesnt exist or mistype + let child = self + .sources + .get(child_path) + .ok_or_else(|| TreeError::FileNotFound { + importing: self.graph[tup.parent.unwrap()].clone(), + missing: child_path.clone(), + })? + .as_ref(); + + Ok(FilialTuple { child, parent }) + }) + }; + + if root_ancestors.is_empty() { + if !is_top_level(&entry.strip_prefix(&self.root)) { + return Err(TreeError::NonTopLevel(entry.clone())); + } + + let dfs = Dfs::new(&self.graph, node) + .into_iter() + .map(transform_cycle_error) + .map(node_to_sourcefile); + trees.push(Ok(dfs)); + } else { + for root in root_ancestors { + let root_path = &self.graph[root]; + if !is_top_level(&root_path.strip_prefix(&self.root)) { + warn!("got a non-valid toplevel file"; "root_ancestor" => root_path); + trees.push(Err(TreeError::NonTopLevel(root_path.clone()))); + continue; + } + + let dfs = Dfs::new(&self.graph, root) + .into_iter() + .map(transform_cycle_error) + .map(node_to_sourcefile); + trees.push(Ok(dfs)); + } + } + + Ok(trees.into_iter()) + } + + /// updates the set of GLSL files connected to the given file, moving unreferenced + pub fn update_sourcefile(&mut self, path: &NormalizedPathBuf) { + let file = self.sources.get(path).unwrap(); + let includes = file.includes().unwrap(); + + info!("includes found for file"; "file" => &file.path, "includes" => format!("{:?}", includes)); + + let idx = self.graph.add_node(&file.path); + + let prev_children: HashSet<_> = + HashSet::from_iter(self.graph.get_all_edges_from(idx).map(|tup| (self.graph[tup.0].clone(), 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.find_node(&removal.0).unwrap(); + self.graph.remove_edge(idx, child, removal.1); + if removal.0.exists() && self.graph.parents(child).count() == 0 { + self.disconnected.insert(removal.0.clone()); + } + } + + // TODO: remove entire subtree from disconnected + for insertion in to_be_added { + let (child, position) = includes.iter().find(|f| f.0 == insertion.0).unwrap().clone(); + let child = self.graph.add_node(&child); + self.graph.add_edge(idx, child, position); + } + } +} + +#[cfg(test)] +mod test { + use crate::{TreeError, WorkspaceTree}; + + #[test] + fn test_trees() { + let mut view = WorkspaceTree::new(&("/home/test/banana".into())); + let parent = view.graph.add_node(&("/home/test/banana/test.fsh".into())); + let child = view.graph.add_node(&("/home/test/banana/included.glsl".into())); + view.graph.add_edge(parent, child, 2.into()); + + let parent = "/home/test/banana/test.fsh".into(); + let trees = view.trees_for_entry(&parent); + match trees { + Ok(_) => panic!("unexpected Ok result"), + Err(e) => match e { + TreeError::NonTopLevel(_) => {} + _ => panic!("unexpected error {:?}", e), + }, + } + } +} diff --git a/server/workspace_tree/src/tree.rs b/server/workspace_tree/src/tree.rs new file mode 100644 index 0000000..053a30d --- /dev/null +++ b/server/workspace_tree/src/tree.rs @@ -0,0 +1,11 @@ +use sourcefile::SourceFile; + +pub struct Tree {} + +impl Iterator for Tree { + type Item = SourceFile; + + fn next(&mut self) -> Option { + todo!() + } +} \ No newline at end of file