├── test ├── tc_dub_empty │ ├── empty1 │ │ └── dub.sdl │ ├── empty2 │ │ └── dub.json │ ├── invalid │ │ └── dub.json │ ├── valid │ │ ├── dub.sdl │ │ └── source │ │ │ └── app.d │ ├── empty3 │ │ └── dub.sdl │ ├── missing_dep │ │ ├── source │ │ │ └── app.d │ │ └── dub.sdl │ ├── sourcelib │ │ ├── source │ │ │ └── lib.d │ │ └── dub.json │ ├── dub.sdl │ ├── empty_windows │ │ └── dub.json │ └── source │ │ └── app.d ├── tc_implement_interface │ ├── .needs_dcd │ ├── tests │ │ ├── implicit_abstract.d.expected │ │ ├── preimplemented.d.expected │ │ ├── poly.d.expected │ │ ├── implicit_abstract.d │ │ ├── nested_types.d.expected │ │ ├── nested_preimplemented.d.expected │ │ ├── poly.d │ │ ├── nested_types.d │ │ ├── preimplemented.d │ │ ├── nested_preimplemented.d │ │ ├── properties.d.expected │ │ ├── properties.d │ │ ├── basic.d.expected │ │ └── basic.d │ ├── dub.sdl │ └── source │ │ └── app.d ├── tc_as_a_exe │ ├── dub.sdl │ └── source │ │ └── app.d ├── tc_dub │ ├── dub.sdl │ └── source │ │ └── app.d ├── tc_fsworkspace │ ├── dub.sdl │ └── source │ │ └── app.d ├── tc_integrated_dfmt │ ├── dub.sdl │ └── source │ │ └── app.d ├── .gitignore ├── tc_integrated_dscanner │ ├── dub.sdl │ └── source │ │ └── app.d ├── data │ ├── snippet_info │ │ ├── dot_exception.d │ │ ├── traits.d │ │ ├── _basic.d │ │ └── identifiers.d │ ├── list_definition │ │ ├── static_ctors.d │ │ ├── _basic.d │ │ └── _verbose.d │ └── dcd │ │ └── download_dcd.d └── runtests.sh ├── .github └── FUNDING.yml ├── dcd ├── dub.sdl └── README.md ├── install.bat ├── install.sh ├── .travis.yml ├── installer ├── dub.sdl └── source │ └── app.d ├── dml ├── dub.json └── source │ ├── app.d │ └── lookupgen.d ├── .gitmodules ├── .env.fish ├── .gitignore ├── source ├── workspaced │ ├── com │ │ ├── fsworkspace.d │ │ ├── snippets │ │ │ ├── _package_tests.d │ │ │ ├── dependencies.d │ │ │ ├── smart.d │ │ │ └── generator.d │ │ ├── dmd.d │ │ ├── ccdb.d │ │ ├── dlangui.d │ │ ├── dfmt.d │ │ └── moduleman.d │ ├── coms.d │ ├── info.d │ ├── dub │ │ ├── diagnostics.d │ │ └── lintgenerator.d │ ├── future.d │ ├── helpers.d │ ├── visitors │ │ ├── classifier.d │ │ └── methodfinder.d │ └── dparseext.d └── app.d ├── LICENSE ├── .editorconfig ├── dub.json ├── package.bat ├── makedeb.d ├── README.md └── views └── dml-completion.txt /test/tc_dub_empty/empty1/dub.sdl: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/tc_dub_empty/empty2/dub.json: -------------------------------------------------------------------------------- 1 | {} -------------------------------------------------------------------------------- /test/tc_implement_interface/.needs_dcd: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/tc_dub_empty/invalid/dub.json: -------------------------------------------------------------------------------- 1 | {"a":/ -------------------------------------------------------------------------------- /test/tc_dub_empty/valid/dub.sdl: -------------------------------------------------------------------------------- 1 | name "valid" -------------------------------------------------------------------------------- /test/tc_dub_empty/empty3/dub.sdl: -------------------------------------------------------------------------------- 1 | name "empty3" -------------------------------------------------------------------------------- /test/tc_as_a_exe/dub.sdl: -------------------------------------------------------------------------------- 1 | name "test-as-a-exe" 2 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: [WebFreak001] 2 | patreon: WebFreak 3 | -------------------------------------------------------------------------------- /test/tc_dub_empty/missing_dep/source/app.d: -------------------------------------------------------------------------------- 1 | void main() 2 | { 3 | } -------------------------------------------------------------------------------- /test/tc_dub_empty/valid/source/app.d: -------------------------------------------------------------------------------- 1 | void main() 2 | { 3 | } 4 | -------------------------------------------------------------------------------- /dcd/dub.sdl: -------------------------------------------------------------------------------- 1 | name "dcd" 2 | 3 | dependency "dcd:common" version="~>0.13.0" 4 | -------------------------------------------------------------------------------- /install.bat: -------------------------------------------------------------------------------- 1 | cd installer 2 | dub build 3 | cd .. 4 | .\installer\iworkspaced . 5 | -------------------------------------------------------------------------------- /test/tc_dub/dub.sdl: -------------------------------------------------------------------------------- 1 | name "test-dub" 2 | 3 | dependency "workspace-d" path="../.." -------------------------------------------------------------------------------- /test/tc_dub_empty/sourcelib/source/lib.d: -------------------------------------------------------------------------------- 1 | module lib; 2 | 3 | void foo() 4 | { 5 | } 6 | -------------------------------------------------------------------------------- /install.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | cd installer 3 | dub build 4 | cd .. 5 | ./installer/iworkspaced . 6 | -------------------------------------------------------------------------------- /test/tc_dub_empty/dub.sdl: -------------------------------------------------------------------------------- 1 | name "test-dub-empty" 2 | 3 | dependency "workspace-d" path="../.." -------------------------------------------------------------------------------- /test/tc_fsworkspace/dub.sdl: -------------------------------------------------------------------------------- 1 | name "test-fsworkspace" 2 | 3 | dependency "workspace-d" path="../.." -------------------------------------------------------------------------------- /test/tc_integrated_dfmt/dub.sdl: -------------------------------------------------------------------------------- 1 | name "test-dfmt" 2 | 3 | dependency "workspace-d" path="../.." -------------------------------------------------------------------------------- /test/.gitignore: -------------------------------------------------------------------------------- 1 | */test-* 2 | testout.txt 3 | *.zip 4 | *.tar.gz 5 | dcd-client* 6 | dcd-server* 7 | -------------------------------------------------------------------------------- /test/tc_dub_empty/sourcelib/dub.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sourcelib", 3 | "targetType": "sourceLibrary" 4 | } -------------------------------------------------------------------------------- /test/tc_implement_interface/tests/implicit_abstract.d.expected: -------------------------------------------------------------------------------- 1 | implement 40 2 | override int prop() @property -------------------------------------------------------------------------------- /test/tc_implement_interface/dub.sdl: -------------------------------------------------------------------------------- 1 | name "test-implement-interface" 2 | 3 | dependency "workspace-d" path="../.." -------------------------------------------------------------------------------- /test/tc_dub_empty/missing_dep/dub.sdl: -------------------------------------------------------------------------------- 1 | name "test-dub-missing-dep" 2 | 3 | dependency "workspace-d:nonexistant" version="*" 4 | -------------------------------------------------------------------------------- /test/tc_integrated_dscanner/dub.sdl: -------------------------------------------------------------------------------- 1 | name "test-dscanner" 2 | 3 | stringImportPaths "source" 4 | 5 | dependency "workspace-d" path="../.." -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: d 2 | d: 3 | - dmd 4 | - ldc 5 | os: 6 | - linux 7 | - osx 8 | script: dub test --compiler=${DC} && cd test && bash runtests.sh ${DC} -------------------------------------------------------------------------------- /test/tc_implement_interface/tests/preimplemented.d.expected: -------------------------------------------------------------------------------- 1 | implement 42 2 | void addChild(Node n) 3 | bool wronglyImplemented() 4 | !string name 5 | void removeChild(Node n) -------------------------------------------------------------------------------- /test/tc_implement_interface/tests/poly.d.expected: -------------------------------------------------------------------------------- 1 | implement 27 - expecting a breadth-first search (for cleaner implementation) 2 | #=1 firstFoo 3 | #=1 secondFoo 4 | #=1 baseFoo 5 | -------------------------------------------------------------------------------- /installer/dub.sdl: -------------------------------------------------------------------------------- 1 | name "installer" 2 | description "Builds workspace-d and its dependencies" 3 | copyright "Copyright © 2017, webfreak" 4 | authors "webfreak" 5 | targetName "iworkspaced" 6 | -------------------------------------------------------------------------------- /dcd/README.md: -------------------------------------------------------------------------------- 1 | DCD client as a library - not depending on libdparse or dsymbol. 2 | 3 | The API for this subpackage is currently not considered stable and may change at any point in time. 4 | -------------------------------------------------------------------------------- /test/tc_implement_interface/tests/implicit_abstract.d: -------------------------------------------------------------------------------- 1 | module implicit_abstract; 2 | 3 | class Foo : Bar 4 | { 5 | } 6 | 7 | class Bar 8 | { 9 | abstract int prop() @property; 10 | } 11 | -------------------------------------------------------------------------------- /test/tc_implement_interface/tests/nested_types.d.expected: -------------------------------------------------------------------------------- 1 | implement 48 2 | !void foo 3 | !void bar 4 | void toImplement() 5 | CoolClass.Helper accessHelper() 6 | CoolClass.Helper2 accessHelper2() -------------------------------------------------------------------------------- /test/tc_dub_empty/empty_windows/dub.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "empty-windows", 3 | "configurations": [ 4 | { 5 | "name": "empty-windows", 6 | "platforms": [ 7 | "posix" 8 | ] 9 | } 10 | ] 11 | } -------------------------------------------------------------------------------- /test/data/snippet_info/dot_exception.d: -------------------------------------------------------------------------------- 1 | void main() 2 | { 3 | foo. 4 | 5 | bar(); 6 | } 7 | 8 | void main() 9 | { 10 | foo.bar 11 | 12 | bar(); 13 | } 14 | 15 | __EOF__ 16 | 19 other 17 | 51 other 18 | 52 other 19 | 54 other 20 | -------------------------------------------------------------------------------- /dml/dub.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "dml", 3 | "targetType": "executable", 4 | "dependencies": { 5 | "pegged": "~>0.3.3", 6 | "dunit": "~>1.0.14" 7 | }, 8 | "buildRequirements": [ 9 | "allowWarnings", 10 | "silenceWarnings" 11 | ] 12 | } -------------------------------------------------------------------------------- /test/data/snippet_info/traits.d: -------------------------------------------------------------------------------- 1 | void foo() 2 | { 3 | static if (__traits(foo)) 4 | { 5 | } 6 | } 7 | 8 | void bar() 9 | { 10 | static if (__traits()) 11 | { 12 | } 13 | } 14 | 15 | __EOF__ 16 | 34 other 17 | 36 other 18 | 37 other 19 | 83 other 20 | -------------------------------------------------------------------------------- /test/tc_implement_interface/tests/nested_preimplemented.d.expected: -------------------------------------------------------------------------------- 1 | implement 52 2 | !string name() 3 | !string tagName() 4 | int numAttributes() @property 5 | string getAttribute(string name) 6 | return super.getAttribute(name); 7 | !dontImplement 8 | void addChild(Node n) -------------------------------------------------------------------------------- /test/tc_implement_interface/tests/poly.d: -------------------------------------------------------------------------------- 1 | module poly; 2 | 3 | class Foo : IFoo1, IFoo2 4 | { 5 | } 6 | 7 | interface IFoo1 : IFoo 8 | { 9 | void firstFoo(); 10 | } 11 | 12 | interface IFoo2 : IFoo 13 | { 14 | void secondFoo(); 15 | } 16 | 17 | interface IFoo 18 | { 19 | void baseFoo(); 20 | } 21 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "dfmt"] 2 | path = dfmt 3 | url = https://github.com/dlang-community/dfmt 4 | [submodule "D-Scanner"] 5 | path = D-Scanner 6 | url = https://github.com/dlang-community/D-Scanner 7 | [submodule "libdparse"] 8 | path = libdparse 9 | url = https://github.com/WebFreak001/libdparse 10 | -------------------------------------------------------------------------------- /.env.fish: -------------------------------------------------------------------------------- 1 | function d 2 | dub run :dml 3 | if dub build $argv 4 | dub run :test 5 | end 6 | end 7 | 8 | function b 9 | dub run :dml 10 | dub build $argv 11 | mv workspace-d ~/etc-bin/workspace-d 12 | killall dcd-server; killall workspace-d 13 | end 14 | 15 | function r 16 | dub run :dml 17 | dub build --build=release $argv 18 | end 19 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .dub 2 | docs.json 3 | __dummy.html 4 | *.o 5 | *.obj 6 | /util/__test__library__ 7 | /test/workspace-d_test 8 | dub.selections.json 9 | *.a 10 | /workspace-d 11 | docs/ 12 | bin/ 13 | iworkspaced 14 | workspace-d_dml 15 | debs/ 16 | *.exe 17 | *.dll 18 | workspace-d*.zip 19 | workspace-d*.tar.xz 20 | __test__*__ 21 | *-test-* 22 | .vscode/ 23 | .vs/ 24 | *.lib 25 | -------------------------------------------------------------------------------- /test/data/list_definition/static_ctors.d: -------------------------------------------------------------------------------- 1 | public class Foo : Bar { 2 | static this() { 3 | for (int i; i < 256; i++) { 4 | arr[i] = i; 5 | } 6 | } 7 | 8 | int[256] arr; 9 | } 10 | __EOF__ 11 | :verbose=true 12 | Foo 1 c {"access":"public"} 23 110 13 | static this() 2 C {"access": "public", "class": "Foo"} 40 92 14 | arr 8 v {"access":"public","class":"Foo"} 105 108 15 | -------------------------------------------------------------------------------- /test/tc_integrated_dfmt/source/app.d: -------------------------------------------------------------------------------- 1 | import std.file; 2 | import std.string; 3 | 4 | import workspaced.api; 5 | import workspaced.coms; 6 | 7 | void main() 8 | { 9 | string dir = getcwd; 10 | scope backend = new WorkspaceD(); 11 | backend.register!DfmtComponent; 12 | 13 | auto dfmt = backend.get!DfmtComponent; 14 | assert(dfmt.format("void main(){}").getBlocking.splitLines.length > 1); 15 | } 16 | -------------------------------------------------------------------------------- /test/tc_implement_interface/tests/nested_types.d: -------------------------------------------------------------------------------- 1 | module nested_types; 2 | 3 | class CoolClassImpl : CoolClass 4 | { 5 | 6 | } 7 | 8 | abstract class CoolClass 9 | { 10 | struct Helper 11 | { 12 | int x, y; 13 | 14 | void foo() 15 | { 16 | } 17 | } 18 | 19 | class Helper2 20 | { 21 | string foo; 22 | 23 | void bar(); 24 | } 25 | 26 | void toImplement(); 27 | Helper accessHelper(); 28 | Helper2 accessHelper2(); 29 | } 30 | -------------------------------------------------------------------------------- /test/tc_implement_interface/tests/preimplemented.d: -------------------------------------------------------------------------------- 1 | module preimplemented; 2 | 3 | class Element : Node, Node2 4 | { 5 | void wronglyImplemented() 6 | { 7 | } 8 | 9 | string name() @property 10 | { 11 | return "foo"; 12 | } 13 | } 14 | 15 | interface Node 16 | { 17 | void addChild(Node n); 18 | bool wronglyImplemented(); 19 | string name() @property; 20 | } 21 | 22 | interface Node2 23 | { 24 | void removeChild(Node n); 25 | } 26 | -------------------------------------------------------------------------------- /test/tc_fsworkspace/source/app.d: -------------------------------------------------------------------------------- 1 | import std.file; 2 | import std.string; 3 | 4 | import workspaced.api; 5 | import workspaced.coms; 6 | 7 | void main() 8 | { 9 | string dir = getcwd; 10 | scope backend = new WorkspaceD(); 11 | auto instance = backend.addInstance(dir); 12 | backend.register!FSWorkspaceComponent; 13 | 14 | auto fsworkspace = backend.get!FSWorkspaceComponent(dir); 15 | 16 | assert(instance.importPaths == [getcwd]); 17 | fsworkspace.addImports(["source"]); 18 | assert(instance.importPaths == [getcwd, "source"]); 19 | } 20 | -------------------------------------------------------------------------------- /test/tc_implement_interface/tests/nested_preimplemented.d: -------------------------------------------------------------------------------- 1 | module nested_preimplemented; 2 | 3 | class HTMLElement : Element 4 | { 5 | string name() @property 6 | { 7 | return "foo"; 8 | } 9 | } 10 | 11 | abstract class Element : Node 12 | { 13 | string tagName() @property 14 | { 15 | return "x"; 16 | } 17 | 18 | abstract int numAttributes() @property; 19 | 20 | abstract string getAttribute(string name) 21 | { 22 | return name; 23 | } 24 | 25 | final void dontImplement() {} 26 | } 27 | 28 | interface Node 29 | { 30 | void addChild(Node n); 31 | string name() @property; 32 | string tagName() @property; 33 | } 34 | -------------------------------------------------------------------------------- /test/tc_implement_interface/tests/properties.d.expected: -------------------------------------------------------------------------------- 1 | implement 33 2 | int tree() @property 3 | return m_tree 4 | void tree(int value) @property 5 | m_tree = value; 6 | 7 | int car() @property 8 | return mCar 9 | void car(int foobar) @property 10 | mCar = foobar; 11 | 12 | int heli() @property 13 | return _heli 14 | void heli(int value) @property 15 | _heli = value; 16 | 17 | int Sharp() @property 18 | return sharp 19 | void Sharp(int value) @property 20 | sharp = value; 21 | 22 | int getJava() 23 | return java 24 | void setJava(int value) 25 | java = value; 26 | 27 | int getter() @property 28 | void setter(int value) @property 29 | -------------------------------------------------------------------------------- /test/data/snippet_info/_basic.d: -------------------------------------------------------------------------------- 1 | /// this is a cool module 2 | module something; 3 | 4 | // todo stuff 5 | 6 | /// a lot of functionality 7 | void foo() 8 | { 9 | writeln("hello world"); 10 | } 11 | 12 | int main(string[] args) 13 | { 14 | foo(); 15 | } 16 | 17 | // TODO: comment snippets not yet implemented 18 | 19 | __EOF__ 20 | 0 global 21 | 1 global 22 | // 2 comment 23 | // 3 docComment 24 | // 10 docComment 25 | // 25 docComment 26 | 26 global 27 | // 47 comment 28 | // 50 comment 29 | // 58 comment 30 | 59 global 31 | 60 global 32 | // 63 docComment 33 | 109 value 34 | 110 strings 35 | 111 strings 36 | 121 strings 37 | 122 other 38 | -------------------------------------------------------------------------------- /test/tc_implement_interface/tests/properties.d: -------------------------------------------------------------------------------- 1 | module properties; 2 | 3 | class Foo : Bar 4 | { 5 | private int m_tree; 6 | private int mCar; 7 | private int _heli; 8 | private int sharp; 9 | private int java; 10 | } 11 | 12 | interface Bar 13 | { 14 | int tree() @property; 15 | void tree(int value) @property; 16 | int car() @property; 17 | void car(int foobar) @property; 18 | int heli() @property; 19 | void heli(int value) @property; 20 | int Sharp() @property; 21 | void Sharp(int value) @property; 22 | int getJava(); 23 | void setJava(int value); 24 | 25 | int getter() @property; 26 | void setter(int value) @property; 27 | } -------------------------------------------------------------------------------- /test/data/snippet_info/identifiers.d: -------------------------------------------------------------------------------- 1 | // ugly indents in this file to make sure there is whitespace at these locations 2 | 3 | void main() 4 | { 5 | foo(); 6 | 7 | bar(); 8 | } 9 | 10 | void foo() 11 | { 12 | bar(); 13 | 14 | } 15 | 16 | void bar() 17 | { 18 | } 19 | 20 | void main() 21 | { 22 | foo(); 23 | 24 | half 25 | 26 | bar(); 27 | } 28 | 29 | void main() 30 | { 31 | foo(); 32 | 33 | while 34 | 35 | bar(); 36 | } 37 | 38 | __EOF__ 39 | 106 method 40 | 140 method 41 | 158 method 42 | 186 method 43 | 188 method 44 | 190 method 45 | 191 other 46 | 227 method 47 | 230 method 48 | 232 method 49 | 233 other 50 | 51 | 101 value 52 | 102 other 53 | -------------------------------------------------------------------------------- /dml/source/app.d: -------------------------------------------------------------------------------- 1 | import std.file; 2 | import std.conv; 3 | import std.string; 4 | 5 | import lookupgen; 6 | 7 | void main() 8 | { 9 | test(); 10 | 11 | string prefix = q{// 12 | // 13 | // DO NOT EDIT 14 | // 15 | // This module has been generated automatically from views/dml-completions.txt using `dub run :dml` 16 | // 17 | // 18 | module workspaced.completion.dml; 19 | 20 | import workspaced.com.dlangui; 21 | 22 | }; 23 | string compStr = "[" ~ generateCompletions(readText("views/dml-completion.txt")) 24 | .to!(string[]).join(",\n\t") ~ "]"; 25 | string completions = "enum dmlCompletions = " ~ compStr ~ ";"; 26 | 27 | write("source/workspaced/completion/dml.d", prefix ~ completions); 28 | } 29 | -------------------------------------------------------------------------------- /test/tc_implement_interface/tests/basic.d.expected: -------------------------------------------------------------------------------- 1 | implement 28 2 | void virtualMethod 3 | // TODO: optional implementation 4 | override int abstractMethod 5 | !package string stringMethod 6 | string stringMethod 7 | Tuple!(int, string, Array!bool)[][] advancedMethod(int a, int b, string c) 8 | void normalMethod() 9 | int attributeSuffixMethod() nothrow @property @nogc 10 | !middleprivate1 11 | !middleprivate2 12 | extern (C) @property @nogc ref immutable int attributePrefixMethod() const 13 | !alreadyImplementedMethod 14 | deprecated("foo") void deprecatedMethod() 15 | !staticMethod 16 | protected void protectedMethod() 17 | !void barfoo 18 | void hello() 19 | int nothrowMethod() nothrow 20 | int nogcMethod() @nogc 21 | nothrow int prefixNothrowMethod() 22 | @nogc int prefixNogcMethod() 23 | void world() 24 | -------------------------------------------------------------------------------- /test/data/list_definition/_basic.d: -------------------------------------------------------------------------------- 1 | module foo.bar; 2 | 3 | version = Foo; 4 | debug = Bar; 5 | 6 | void hello() { 7 | int x = 1; 8 | } 9 | 10 | int y = 2; 11 | 12 | int 13 | bar() 14 | { 15 | } 16 | 17 | unittest 18 | { 19 | } 20 | 21 | @( "named" ) 22 | unittest 23 | { 24 | } 25 | 26 | class X 27 | { 28 | this(int x) {} 29 | this(this) {} 30 | ~this() {} 31 | 32 | unittest 33 | { 34 | } 35 | } 36 | 37 | shared static this() 38 | { 39 | } 40 | 41 | __EOF__ 42 | hello 6 f {"signature": "()", "access": "public", "return": "void"} 59 73 43 | y 10 v {"access": "public"} 80 81 44 | bar 13 f {"signature": "()", "access": "public", "return": "int"} 98 100 45 | X 26 c {"access": "public"} 152 214 46 | this 28 f {"signature": "(int x)", "access": "public", "class": "X"} 167 168 47 | ~this 30 f {"access": "public", "class": "X"} 194 195 48 | -------------------------------------------------------------------------------- /source/workspaced/com/fsworkspace.d: -------------------------------------------------------------------------------- 1 | module workspaced.com.fsworkspace; 2 | 3 | import std.json; 4 | import workspaced.api; 5 | 6 | @component("fsworkspace") 7 | class FSWorkspaceComponent : ComponentWrapper 8 | { 9 | mixin DefaultComponentWrapper; 10 | 11 | protected void load() 12 | { 13 | if (!refInstance) 14 | throw new Exception("fsworkspace requires to be instanced"); 15 | 16 | paths = instance.cwd ~ config.get!(string[])("fsworkspace", "additionalPaths"); 17 | importPathProvider = &imports; 18 | stringImportPathProvider = &imports; 19 | importFilesProvider = &imports; 20 | } 21 | 22 | /// Adds new import paths to the workspace. You can add import paths, string import paths or file paths. 23 | void addImports(string[] values) 24 | { 25 | paths ~= values; 26 | } 27 | 28 | /// Lists all import-, string import- & file import paths 29 | string[] imports() nothrow 30 | { 31 | return paths; 32 | } 33 | 34 | private: 35 | string[] paths; 36 | } 37 | -------------------------------------------------------------------------------- /source/workspaced/coms.d: -------------------------------------------------------------------------------- 1 | module workspaced.coms; 2 | 3 | import std.meta; 4 | 5 | public import workspaced.com.ccdb : ClangCompilationDatabaseComponent; 6 | public import workspaced.com.dcd : DCDComponent; 7 | public import workspaced.com.dcdext : DCDExtComponent; 8 | public import workspaced.com.dfmt : DfmtComponent; 9 | public import workspaced.com.dlangui : DlanguiComponent; 10 | public import workspaced.com.dmd : DMDComponent; 11 | public import workspaced.com.dscanner : DscannerComponent; 12 | public import workspaced.com.dub : DubComponent; 13 | public import workspaced.com.fsworkspace : FSWorkspaceComponent; 14 | public import workspaced.com.importer : ImporterComponent; 15 | public import workspaced.com.moduleman : ModulemanComponent; 16 | public import workspaced.com.snippets : SnippetsComponent; 17 | 18 | alias AllComponents = AliasSeq!(ClangCompilationDatabaseComponent, DCDComponent, 19 | DfmtComponent, DlanguiComponent, DscannerComponent, DubComponent, 20 | FSWorkspaceComponent, ImporterComponent, ModulemanComponent, 21 | DCDExtComponent, DMDComponent, SnippetsComponent); 22 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2017 Jan Jurzitza 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | [*] 2 | max_line_length = 170 3 | indent_style = tab 4 | 5 | [*.d] 6 | # allman, otbs or stroustrup - see https://en.wikipedia.org/wiki/Indent_style 7 | dfmt_brace_style = allman 8 | # The formatting process will usually keep lines below this length, but they may be up to max_line_length columns long. 9 | dfmt_soft_max_line_length = 160 10 | # Place operators on the end of the previous line when splitting lines 11 | dfmt_split_operator_at_line_end = false 12 | # Insert space after the closing paren of a cast expression 13 | dfmt_space_after_cast = true 14 | # Insert space after the module name and before the : for selective imports 15 | dfmt_selective_import_space = true 16 | # Place labels on the same line as the labeled switch, for, foreach, or while statement 17 | dfmt_compact_labeled_statements = true 18 | # 19 | # Not yet implemented: 20 | # 21 | # Align labels, cases, and defaults with their enclosing switch 22 | dfmt_align_switch_statements = true 23 | # Decrease the indentation level of attributes 24 | dfmt_outdent_attributes = true 25 | # Insert space after if, while, foreach, etc, and before the ( 26 | dfmt_space_after_keywords = true 27 | -------------------------------------------------------------------------------- /dub.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "workspace-d", 3 | "description": "Provides functions for IDEs for managing DCD, Dscanner and Dfmt. Usable as application and library", 4 | "license": "MIT", 5 | "copyright": "Copyright © 2017-2021, webfreak", 6 | "authors": [ 7 | "webfreak" 8 | ], 9 | "dependencies": { 10 | "workspace-d:dcd": "*", 11 | "dub": "1.28.0-beta.1", 12 | "painlessjson": "1.4.0", 13 | "standardpaths": "0.8.2", 14 | "dfmt": "0.14.2", 15 | "dscanner": "0.12.0", 16 | "inifiled": "1.3.3", 17 | "libdparse": "0.19.0", 18 | "emsi_containers": "0.8.0" 19 | }, 20 | "subPackages": [ 21 | "./installer", 22 | "./dcd", 23 | "./dml" 24 | ], 25 | "configurations": [ 26 | { 27 | "name": "executable", 28 | "targetType": "executable", 29 | "mainSourceFile": "source/app.d" 30 | }, 31 | { 32 | "name": "library", 33 | "targetType": "library", 34 | "excludedSourceFiles": [ 35 | "source/app.d" 36 | ] 37 | }, 38 | { 39 | "name": "unittest", 40 | "mainSourceFile": "source/app.d", 41 | "dflags": ["-checkaction=context"], 42 | "dependencies": { 43 | "silly": "~>1.1.1" 44 | } 45 | } 46 | ] 47 | } 48 | -------------------------------------------------------------------------------- /test/tc_implement_interface/tests/basic.d: -------------------------------------------------------------------------------- 1 | module basic; 2 | 3 | class Bar : Foo 4 | { 5 | } 6 | 7 | class Foo : Foo0 8 | { 9 | void virtualMethod(); 10 | abstract int abstractMethod(string s) { return cast(int) s.length; } 11 | } 12 | 13 | import std.container.array; 14 | import std.typecons; 15 | package interface Foo0 : Foo1, Foo2 16 | { 17 | string stringMethod(); 18 | Tuple!(int, string, Array!bool)[][] advancedMethod(int a, int b, string c); 19 | void normalMethod(); 20 | int attributeSuffixMethod() nothrow @property @nogc; 21 | private 22 | { 23 | void middleprivate1(); 24 | void middleprivate2(); 25 | } 26 | extern(C) @property @nogc ref immutable int attributePrefixMethod() const; 27 | final void alreadyImplementedMethod() {} 28 | deprecated("foo") void deprecatedMethod() {} 29 | static void staticMethod() {} 30 | protected void protectedMethod(); 31 | private: 32 | void barfoo(); 33 | } 34 | 35 | interface Foo1 36 | { 37 | void hello(); 38 | int nothrowMethod() nothrow; 39 | int nogcMethod() @nogc; 40 | nothrow int prefixNothrowMethod(); 41 | @nogc int prefixNogcMethod(); 42 | } 43 | 44 | interface Foo2 45 | { 46 | void world(); 47 | } -------------------------------------------------------------------------------- /package.bat: -------------------------------------------------------------------------------- 1 | rem Building & compressing serve-d for release inside a virtual machine with Windows 8 or above 2 | 3 | pushd %~dp0 4 | 5 | @if not exist version.txt ( 6 | echo. 7 | echo !-- Error: version.txt is missing :/ 8 | echo. 9 | pause 10 | popd 11 | goto :eof 12 | ) 13 | 14 | rem This will sync this repo with the folder %SystemDrive%\buildwd 15 | robocopy . %SystemDrive%\buildwd /MIR /XA:SH /XD .* /XF .* /XF *.zip 16 | pushd %SystemDrive%\buildwd 17 | 18 | set /p Version== 3); 24 | auto defs = dscanner.listDefinitions("app.d", import("app.d")).getBlocking; 25 | assert(defs.length == 2); 26 | assert(defs[0].name == "mainLine"); 27 | assert(defs[0].line == mainLine - 1); 28 | assert(defs[0].type == "v"); 29 | 30 | assert(defs[1].name == "main"); 31 | assert(defs[1].line == mainLine); 32 | assert(defs[1].type == "f"); 33 | assert(defs[1].attributes.length >= 1); 34 | assert(defs[1].attributes["signature"] == "()"); 35 | 36 | backend.register!FSWorkspaceComponent; 37 | auto fsworkspace = backend.get!FSWorkspaceComponent(dir); 38 | 39 | fsworkspace.addImports(["source"]); 40 | assert(dscanner.findSymbol("main") 41 | .getBlocking[0] == FileLocation(buildNormalizedPath(dir, "source/app.d"), mainLine, 6)); 42 | } 43 | -------------------------------------------------------------------------------- /test/runtests.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | RED="\033[31m" 4 | GREEN="\033[32m" 5 | YELLOW="\033[33m" 6 | NORMAL="\033[0m" 7 | 8 | COMPILER="$1" 9 | 10 | if [ -z $COMPILER ]; then 11 | COMPILER="dmd" 12 | fi 13 | 14 | fail_count=0 15 | pass_count=0 16 | 17 | echo "Compiling workspace-d in release mode with ${COMPILER}..." 18 | 19 | pushd .. 20 | dub build --build=release --compiler="${COMPILER}" 21 | popd 22 | 23 | tests="${@:2}" 24 | if [ -z "$tests" ]; then 25 | tests=tc* 26 | fi 27 | 28 | echo "Running tests with ${COMPILER}..." 29 | 30 | for testCase in $tests; do 31 | echo -e "${YELLOW}$testCase${NORMAL}" 32 | pushd $testCase 33 | 34 | if [ -f .needs_dcd ]; then 35 | pushd ../data/dcd 36 | dmd -I../../../source -run download_dcd.d 37 | popd 38 | cp ../data/dcd/dcd-server* . 39 | cp ../data/dcd/dcd-client* . 40 | fi 41 | 42 | dub upgrade >testout.txt 2>&1 43 | dub --compiler="${COMPILER}" >>testout.txt 2>&1 44 | if [[ $? -eq 0 ]]; then 45 | echo -e "${YELLOW}$testCase:${NORMAL} ... ${GREEN}Pass${NORMAL}"; 46 | let pass_count=pass_count+1 47 | else 48 | echo -e "${YELLOW}$testCase:${NORMAL} ... ${RED}Fail${NORMAL}"; 49 | cat testout.txt 50 | let fail_count=fail_count+1 51 | fi 52 | 53 | popd 54 | done 55 | 56 | if [[ $fail_count -eq 0 ]]; then 57 | echo -e "${GREEN}${pass_count} tests passed and ${fail_count} failed.${NORMAL}" 58 | else 59 | echo -e "${RED}${pass_count} tests passed and ${fail_count} failed.${NORMAL}" 60 | exit 1 61 | fi -------------------------------------------------------------------------------- /source/workspaced/info.d: -------------------------------------------------------------------------------- 1 | module workspaced.info; 2 | 3 | import Compiler = std.compiler; 4 | import OS = std.system; 5 | import std.conv; 6 | import std.json; 7 | 8 | static immutable Version = [3, 8, 0]; 9 | static immutable string BundledDependencies = "dub, dfmt and dscanner are bundled within (compiled in)"; 10 | 11 | static immutable latestKnownDCDVersion = [0, 13, 6]; 12 | 13 | version (Windows) version (DigitalMars) static assert(false, 14 | "DMD not supported on Windows. Please use LDC."); 15 | 16 | string getVersionInfoString() 17 | { 18 | return Version[0].to!string ~ '.' ~ Version[1].to!string ~ '.' 19 | ~ Version[2].to!string ~ " compiled with " ~ Compiler.name ~ " v" 20 | ~ Compiler.version_major.to!string ~ "." 21 | ~ Compiler.version_minor.to!string ~ " - " ~ OS.os.to!string ~ " " 22 | ~ OS.endian.to!string ~ ". " ~ BundledDependencies; 23 | } 24 | 25 | JSONValue getVersionInfoJson() 26 | { 27 | //dfmt off 28 | return JSONValue([ 29 | "major": JSONValue(Version[0]), 30 | "minor": JSONValue(Version[1]), 31 | "patch": JSONValue(Version[2]), 32 | "compiler": JSONValue([ 33 | "name": JSONValue(Compiler.name), 34 | "vendor": JSONValue(Compiler.vendor.to!string), 35 | "major": JSONValue(Compiler.version_major.to!string), 36 | "minor": JSONValue(Compiler.version_minor.to!string) 37 | ]), 38 | "os": JSONValue(OS.os.to!string), 39 | "endian": JSONValue(OS.endian.to!string), 40 | "summary": JSONValue(getVersionInfoString) 41 | ]); 42 | //dfmt on 43 | } 44 | -------------------------------------------------------------------------------- /test/tc_dub/source/app.d: -------------------------------------------------------------------------------- 1 | import std.conv : to; 2 | import std.file; 3 | import std.path; 4 | import std.string; 5 | 6 | import workspaced.api; 7 | import workspaced.com.dub; 8 | 9 | void main() 10 | { 11 | string dir = buildNormalizedPath(getcwd, "..", "tc_fsworkspace"); 12 | scope backend = new WorkspaceD(); 13 | auto instance = backend.addInstance(dir); 14 | backend.register!DubComponent; 15 | 16 | auto dub = backend.get!DubComponent(dir); 17 | 18 | dub.upgrade(); 19 | assert(dub.dependencies.length > 2); 20 | assert(dub.rootDependencies == ["workspace-d"]); 21 | // this can be 22 | // tc_fsworkspace/source, workspace-d/source 23 | // if no dependencies are fetched 24 | // or with all dependencies there a lot more 25 | assert(dub.imports.length >= 2, dub.imports.to!string); 26 | assert(dub.stringImports[0].endsWith("views") 27 | || dub.stringImports[0].endsWith("views/") || dub.stringImports[0].endsWith("views\\")); 28 | assert(dub.fileImports.length > 10); 29 | assert(dub.configurations.length == 2); 30 | assert(dub.buildTypes.length); 31 | assert(dub.configuration == "application"); 32 | assert(dub.archTypes.length); 33 | assert(dub.archType.length); 34 | assert(dub.buildType == "debug"); 35 | assert(dub.compiler.length); 36 | assert(dub.name == "test-fsworkspace"); 37 | assert(dub.path.toString.endsWith("tc_fsworkspace") 38 | || dub.path.toString.endsWith("tc_fsworkspace/") 39 | || dub.path.toString.endsWith("tc_fsworkspace\\")); 40 | if (dub.canBuild) 41 | assert(dub.build.getBlocking.count!(a => a.type == ErrorType.Warning || a.type == ErrorType.Error) == 0); 42 | } 43 | -------------------------------------------------------------------------------- /makedeb.d: -------------------------------------------------------------------------------- 1 | import std.stdio : writeln; 2 | import std.file; 3 | import std.path; 4 | import std.conv; 5 | import std.process; 6 | 7 | static import std.stdio; 8 | 9 | import workspaced.info; 10 | 11 | void main() 12 | { 13 | if (!exists("debs")) 14 | mkdir("debs"); 15 | auto pkgVersion = Version[0].to!string ~ "." ~ Version[1].to!string ~ "-" ~ Version[2].to!string; 16 | string pkgPath = "workspace-d_" ~ pkgVersion; 17 | if (exists("debs/" ~ pkgPath)) 18 | { 19 | writeln("Package already exists, returning"); 20 | return; 21 | } 22 | mkdir("debs/" ~ pkgPath); 23 | mkdir("debs/" ~ pkgPath ~ "/DEBIAN"); 24 | write("debs/" ~ pkgPath ~ "/DEBIAN/control", `Package: workspace-d 25 | Version: ` ~ pkgVersion ~ ` 26 | Section: base 27 | Priority: optional 28 | Architecture: amd64 29 | Maintainer: WebFreak001 30 | Description: Wraps dcd, dfmt and dscanner to one unified environment managed by dub 31 | `); 32 | mkdir("debs/" ~ pkgPath ~ "/usr"); 33 | mkdir("debs/" ~ pkgPath ~ "/usr/local"); 34 | mkdir("debs/" ~ pkgPath ~ "/usr/local/bin"); 35 | writeln("Building workspace-d"); 36 | spawnProcess(["dub", "build", "--compiler=ldc", "--build=release"]).wait; 37 | writeln("Compressing regular linux package"); 38 | spawnProcess(["tar", "cfJ", 39 | "workspace-d_" ~ Version[0].to!string ~ "." ~ Version[1].to!string ~ "." 40 | ~ Version[2].to!string ~ "-linux-x86_64.tar.xz", "workspace-d"]).wait; 41 | rename("workspace-d", "debs/" ~ pkgPath ~ "/usr/local/bin/workspace-d"); 42 | writeln("Generating package in debs/ folder"); 43 | spawnProcess(["dpkg-deb", "--build", pkgPath], std.stdio.stdin, 44 | std.stdio.stdout, std.stdio.stderr, null, Config.none, buildPath(getcwd, "debs")).wait; 45 | writeln("Done"); 46 | } 47 | -------------------------------------------------------------------------------- /test/data/dcd/download_dcd.d: -------------------------------------------------------------------------------- 1 | import std.algorithm; 2 | import std.conv; 3 | import std.file; 4 | import std.format; 5 | import std.process; 6 | import std.stdio; 7 | import std.string; 8 | 9 | void downloadFile(string path, string url) 10 | { 11 | import std.net.curl : download; 12 | 13 | if (exists(path)) 14 | return; 15 | 16 | stderr.writeln("Downloading ", url, " to ", path); 17 | 18 | download(url, path); 19 | } 20 | 21 | void extractZip(string path) 22 | { 23 | import std.zip : ZipArchive; 24 | 25 | auto zip = new ZipArchive(read(path)); 26 | foreach (name, am; zip.directory) 27 | { 28 | stderr.writeln("Unpacking ", name); 29 | zip.expand(am); 30 | 31 | if (exists(name)) 32 | remove(name); 33 | 34 | std.file.write(name, am.expandedData); 35 | } 36 | } 37 | 38 | static immutable latestKnownVersion = (){ 39 | import workspaced.info : latestKnownDCDVersion; 40 | 41 | return latestKnownDCDVersion; 42 | }(); 43 | 44 | void main() 45 | { 46 | string ver = format!"%(%s.%)"(latestKnownVersion); 47 | string dcdClient = "dcd-client"; 48 | string dcdServer = "dcd-server"; 49 | version (Windows) 50 | { 51 | dcdClient ~= ".exe"; 52 | dcdServer ~= ".exe"; 53 | string zip = "dcd-" ~ ver ~ ".zip"; 54 | string url = format!"https://github.com/dlang-community/DCD/releases/download/v%s/dcd-v%s-windows-x86_64.zip"(ver, ver); 55 | void extract() 56 | { 57 | extractZip(zip); 58 | } 59 | } 60 | else version (linux) 61 | { 62 | string zip = "dcd-v" ~ ver ~ "-linux-x86_64.tar.gz"; 63 | string url = format!"https://github.com/dlang-community/DCD/releases/download/v%s/dcd-v%s-linux-x86_64.tar.gz"(ver, ver); 64 | void extract() 65 | { 66 | spawnShell("tar -xzvf " ~ zip ~ " > /dev/null").wait; 67 | } 68 | } 69 | else version (OSX) 70 | { 71 | string zip = "dcd-v" ~ ver ~ "-osx-x86_64.tar.gz"; 72 | string url = format!"https://github.com/dlang-community/DCD/releases/download/v%s/dcd-v%s-osx-x86_64.tar.gz"(ver, ver); 73 | void extract() 74 | { 75 | spawnShell("tar -xzvf " ~ zip ~ " > /dev/null").wait; 76 | } 77 | } 78 | 79 | if (!exists(zip)) 80 | { 81 | writeln("Downloading DCD ", ver); 82 | downloadFile(zip, url); 83 | } 84 | 85 | try { remove(dcdClient); } catch (FileException) { /* ignore */ } 86 | try { remove(dcdServer); } catch (FileException) { /* ignore */ } 87 | 88 | extract(); 89 | } 90 | 91 | -------------------------------------------------------------------------------- /source/workspaced/com/snippets/_package_tests.d: -------------------------------------------------------------------------------- 1 | module workspaced.com.snippets._package_tests; 2 | 3 | import std.conv; 4 | 5 | import workspaced.api; 6 | import workspaced.com.dfmt; 7 | import workspaced.com.snippets; 8 | import workspaced.helpers; 9 | 10 | unittest 11 | { 12 | scope backend = new WorkspaceD(); 13 | auto workspace = makeTemporaryTestingWorkspace; 14 | auto instance = backend.addInstance(workspace.directory); 15 | backend.register!SnippetsComponent; 16 | backend.register!DfmtComponent; 17 | SnippetsComponent snippets = backend.get!SnippetsComponent(workspace.directory); 18 | 19 | auto args = ["--indent_style", "tab"]; 20 | 21 | auto res = snippets.formatSync("void main(${1:string[] args}) {\n\t$0\n}", args); 22 | assert(res == "void main(${1:string[] args})\n{\n\t$0\n}"); 23 | 24 | res = snippets.formatSync("class ${1:MyClass} {\n\t$0\n}", args); 25 | assert(res == "class ${1:MyClass}\n{\n\t$0\n}"); 26 | 27 | res = snippets.formatSync("enum ${1:MyEnum} = $2;\n$0", args); 28 | assert(res == "enum ${1:MyEnum} = $2;\n$0"); 29 | 30 | res = snippets.formatSync("import ${1:std};\n$0", args); 31 | assert(res == "import ${1:std};\n$0"); 32 | 33 | res = snippets.formatSync("import ${1:std};\n$0", args, SnippetLevel.method); 34 | assert(res == "import ${1:std};\n$0"); 35 | 36 | res = snippets.formatSync("foo(delegate() {\n${1:// foo}\n});", args, SnippetLevel.method); 37 | assert(res == "foo(delegate() {\n\t${1:// foo}\n});"); 38 | 39 | res = snippets.formatSync(`auto ${1:window} = new SimpleWindow(Size(${2:800, 600}), "$3");`, args, SnippetLevel.method); 40 | assert(res == `auto ${1:window} = new SimpleWindow(Size(${2:800, 600}), "$3");`); 41 | } 42 | 43 | unittest 44 | { 45 | import workspaced.helpers; 46 | 47 | scope backend = new WorkspaceD(); 48 | auto workspace = makeTemporaryTestingWorkspace; 49 | auto instance = backend.addInstance(workspace.directory); 50 | backend.register!SnippetsComponent; 51 | SnippetsComponent snippets = backend.get!SnippetsComponent(workspace.directory); 52 | 53 | runTestDataFileTests("test/data/snippet_info", null, null, 54 | (code, parts, line) { 55 | assert(parts.length == 2, "malformed snippet info test line: " ~ line); 56 | 57 | auto i = snippets.determineSnippetInfo(null, code, parts[0].to!int); 58 | assert(i.level == parts[1].to!SnippetLevel, i.stack.to!string); 59 | }, null); 60 | } -------------------------------------------------------------------------------- /source/workspaced/dub/diagnostics.d: -------------------------------------------------------------------------------- 1 | module workspaced.dub.diagnostics; 2 | 3 | import workspaced.api; 4 | 5 | import std.algorithm; 6 | import std.string; 7 | 8 | import dparse.ast; 9 | import dparse.lexer; 10 | import dparse.parser; 11 | import dparse.rollback_allocator; 12 | 13 | int[2] resolveDubDiagnosticRange(scope const(char)[] code, 14 | scope const(Token)[] tokens, Module parsed, int position, 15 | scope const(char)[] diagnostic) 16 | { 17 | if (diagnostic.startsWith("use `is` instead of `==`", 18 | "use `!is` instead of `!=`")) 19 | { 20 | auto expr = new EqualComparisionFinder(position); 21 | expr.visit(parsed); 22 | if (expr.result !is null) 23 | { 24 | const left = &expr.result.left.tokens[$ - 1]; 25 | const right = &expr.result.right.tokens[0]; 26 | auto between = left[1 .. right - left]; 27 | const tok = between[0]; 28 | if (tok.type == expr.result.operator) 29 | { 30 | auto index = cast(int) tok.index; 31 | return [index, index + 2]; 32 | } 33 | } 34 | } 35 | return [position, position]; 36 | } 37 | 38 | /// Finds the equals comparision at the given index. 39 | /// Used to resolve issue locations for diagnostics of type 40 | /// - use `is` instead of `==` 41 | /// - use `!is` instead of `!=` 42 | class EqualComparisionFinder : ASTVisitor 43 | { 44 | this(size_t index) 45 | { 46 | this.index = index; 47 | } 48 | 49 | override void visit(const(CmpExpression) expr) 50 | { 51 | if (expr.equalExpression !is null) 52 | { 53 | const start = expr.tokens[0].index; 54 | const last = expr.tokens[$ - 1]; 55 | const end = last.index + last.text.length; 56 | if (index >= start && index < end) 57 | { 58 | result = cast(EqualExpression) expr.equalExpression; 59 | } 60 | } 61 | super.visit(expr); 62 | } 63 | 64 | alias visit = ASTVisitor.visit; 65 | size_t index; 66 | EqualExpression result; 67 | } 68 | 69 | unittest 70 | { 71 | string code = q{void main() { 72 | if (foo(a == 4) == null) 73 | { 74 | } 75 | }}; 76 | 77 | LexerConfig config; 78 | RollbackAllocator rba; 79 | StringCache cache = StringCache(64); 80 | auto tokens = getTokensForParser(cast(ubyte[]) code, config, &cache); 81 | auto parsed = parseModule(tokens, "equal_finder.d", &rba); 82 | 83 | auto range = resolveDubDiagnosticRange(code, tokens, parsed, 19, 84 | "use `is` instead of `==` when comparing with `null`"); 85 | 86 | assert(range == [31, 33]); 87 | } 88 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # THE workspace-d CLI IS NO LONGER MAINTAINED - API/LIBRARY USERS SEE [serve-d:workspace-d](https://github.com/Pure-D/serve-d/tree/master/workspace-d) 2 | 3 | workspace-d started out as executable for different IDEs to implement D functionality within them back before LSP was a thing. Over time all functionality got exposed using workspace-d as a library in a new program called [serve-d](https://github.com/Pure-D/serve-d) which is a server implementation of the Microsoft Language Server Protocol. (LSP) 4 | 5 | As the LSP server has been very stable for a while and workspace-d as a standalone not being used anymore I decided to deprecate the workspace-d command line interface with its proprietary RPC protocol. 6 | 7 | The source code of workspace-d now lives in serve-d as a subpackage ([serve-d:workspace-d](https://github.com/Pure-D/serve-d/tree/master/workspace-d)). 8 | 9 | As the proprietary RPC protocol has been removed a lot of template code has been removed and compilation times as library have sped up. Furthermore it's possible to use all of D's features in workspace-d APIs now, without needing to take care of the custom RPC protocol. 10 | 11 | --- 12 | 13 | Old README: 14 | 15 | # workspace-d [![Build Status](https://travis-ci.org/Pure-D/workspace-d.svg?branch=master)](https://travis-ci.org/Pure-D/workspace-d) 16 | 17 | Join the chat: [![Join on Discord](https://discordapp.com/api/guilds/242094594181955585/widget.png?style=shield)](https://discord.gg/Bstj9bx) 18 | 19 | workspace-d wraps dcd, dfmt and dscanner to one unified environment managed by dub. 20 | 21 | It uses process pipes and json for communication. 22 | 23 | ## Special Thanks 24 | 25 | **Thanks to the following big GitHub sponsors** financially supporting the code-d/serve-d tools: 26 | 27 | * Jaen ([@jaens](https://github.com/jaens)) 28 | 29 | _[become a sponsor](https://github.com/sponsors/WebFreak001)_ 30 | 31 | ## Installation 32 | 33 | [Precompiled binaries for windows & linux](https://github.com/Pure-D/workspace-d/releases) 34 | 35 | **Automatic Installation** 36 | 37 | Just run install.sh or install.bat (Windows/WIP) 38 | 39 | ```sh 40 | sh install.sh 41 | ``` 42 | 43 | **Manual Installation** 44 | 45 | First, install the dependencies: 46 | 47 | * [dcd](https://github.com/dlang-community/DCD) - Used for auto completion 48 | * [dfmt](https://github.com/dlang-community/dfmt) - Used for code formatting 49 | * [dscanner](https://github.com/dlang-community/Dscanner) - Used for static code linting 50 | 51 | Then, run: 52 | 53 | ```sh 54 | git clone https://github.com/Pure-D/workspace-d.git 55 | cd workspace-d 56 | git submodule init 57 | git submodule update 58 | dub build --build=release --compiler=ldc2 59 | ``` 60 | 61 | Either move all the executable binaries to one path and add that path to the Windows PATH 62 | variable or $PATH on Posix, or change the binary path configuration in your editor. 63 | 64 | ## Usage 65 | 66 | **For users** 67 | 68 | * Visual Studio Code: [code-d](https://github.com/Pure-D/code-d) 69 | 70 | **For plugin developers** 71 | 72 | Microsoft Language Server Protocol (LSP) wrapper: [serve-d](https://github.com/Pure-D/serve-d) 73 | 74 | [Wiki/Message Format](https://github.com/Pure-D/workspace-d/wiki/Message-Format) 75 | 76 | -------------------------------------------------------------------------------- /test/tc_dub_empty/source/app.d: -------------------------------------------------------------------------------- 1 | import Compiler = std.compiler; 2 | import std.file; 3 | import std.json; 4 | import std.path; 5 | import std.stdio; 6 | import std.string; 7 | import std.traits; 8 | 9 | import workspaced.api; 10 | import workspaced.coms; 11 | 12 | WorkspaceD backend; 13 | 14 | void main() 15 | { 16 | string dir = buildNormalizedPath(getcwd, "..", "tc_fsworkspace"); 17 | backend = new WorkspaceD(); 18 | backend.register!DubComponent; 19 | scope (exit) 20 | backend.shutdown(); 21 | 22 | assert(tryDub("valid")); 23 | assert(!tryDub("empty1")); 24 | assert(!tryDub("empty2")); 25 | assert(tryDub("empty3")); 26 | assert(!tryDub("invalid")); 27 | assert(tryDub("sourcelib")); 28 | version (Windows) 29 | assert(tryDub("empty_windows")); 30 | assert(tryDub("missing_dep")); 31 | stderr.writeln("Success!"); 32 | } 33 | 34 | bool tryDub(string path) 35 | { 36 | DubComponent dub; 37 | try 38 | { 39 | if (exists(buildNormalizedPath(getcwd, path, ".dub"))) 40 | rmdirRecurse(buildNormalizedPath(getcwd, path, ".dub")); 41 | if (exists(buildNormalizedPath(getcwd, path, "dub.selections.json"))) 42 | remove(buildNormalizedPath(getcwd, path, "dub.selections.json")); 43 | 44 | auto dir = buildNormalizedPath(getcwd, path); 45 | backend.addInstance(dir); 46 | dub = backend.get!DubComponent(dir); 47 | } 48 | catch (Exception e) 49 | { 50 | stderr.writeln(path, ": ", e.msg); 51 | return false; 52 | } 53 | 54 | auto tryRun(string fn, Args...)(string trace, Args args) 55 | { 56 | try 57 | { 58 | scope (success) 59 | stderr.writeln(trace, ": pass ", fn); 60 | mixin("return dub." ~ fn ~ "(args);"); 61 | } 62 | catch (Exception e) 63 | { 64 | stderr.writeln(trace, ": failed to run ", fn, ": ", e.msg); 65 | static if (!is(typeof(return) == void)) 66 | return typeof(return).init; 67 | } 68 | catch (Error e) 69 | { 70 | stderr.writeln(trace, ": assert error in ", fn, ": ", e.msg); 71 | throw e; 72 | } 73 | } 74 | 75 | foreach (step; 0 .. 2) 76 | { 77 | tryRun!"upgrade"(path); 78 | tryRun!"dependencies"(path); 79 | tryRun!"rootDependencies"(path); 80 | tryRun!"imports"(path); 81 | tryRun!"stringImports"(path); 82 | tryRun!"fileImports"(path); 83 | tryRun!"configurations"(path); 84 | tryRun!"buildTypes"(path); 85 | tryRun!"configuration"(path); 86 | tryRun!"setConfiguration"(path, dub.configuration); 87 | tryRun!"archTypes"(path); 88 | tryRun!"archType"(path); 89 | tryRun!"setArchType"(path, JSONValue(["arch-type" : JSONValue("x86")])); 90 | tryRun!"buildType"(path); 91 | tryRun!"setBuildType"(path, JSONValue(["build-type" : JSONValue("debug")])); 92 | tryRun!"compiler"(path); 93 | static if (Compiler.vendor == Compiler.Vendor.gnu) 94 | tryRun!"setCompiler"(path, "gdc"); 95 | else static if (Compiler.vendor == Compiler.Vendor.llvm) 96 | tryRun!"setCompiler"(path, "ldc2"); 97 | else 98 | tryRun!"setCompiler"(path, "dmd"); 99 | tryRun!"name"(path); 100 | tryRun!"path"(path); 101 | tryRun!"build.getBlocking"(path); 102 | // restart 103 | tryRun!"update.getBlocking"(path); 104 | } 105 | 106 | return true; 107 | } 108 | -------------------------------------------------------------------------------- /source/workspaced/future.d: -------------------------------------------------------------------------------- 1 | module workspaced.future; 2 | 3 | import core.time; 4 | 5 | import std.parallelism; 6 | import std.traits : isCallable; 7 | 8 | class Future(T) 9 | { 10 | import core.thread : Fiber, Thread; 11 | 12 | static if (!is(T == void)) 13 | T value; 14 | Throwable exception; 15 | bool has; 16 | void delegate() _onDone; 17 | private Thread _worker; 18 | 19 | /// Sets the onDone callback if no value has been set yet or calls immediately if the value has already been set or was set during setting the callback. 20 | /// Crashes with an assert error if attempting to override an existing callback (i.e. calling this function on the same object twice). 21 | void onDone(void delegate() callback) @property 22 | { 23 | assert(!_onDone); 24 | if (has) 25 | callback(); 26 | else 27 | { 28 | bool called; 29 | _onDone = { called = true; callback(); }; 30 | if (has && !called) 31 | callback(); 32 | } 33 | } 34 | 35 | static if (is(T == void)) 36 | static Future!void finished() 37 | { 38 | auto ret = new typeof(return); 39 | ret.has = true; 40 | return ret; 41 | } 42 | else 43 | static Future!T fromResult(T value) 44 | { 45 | auto ret = new typeof(return); 46 | ret.value = value; 47 | ret.has = true; 48 | return ret; 49 | } 50 | 51 | static Future!T async(T delegate() cb) 52 | { 53 | auto ret = new typeof(return); 54 | ret._worker = new Thread({ 55 | try 56 | { 57 | static if (is(T == void)) 58 | { 59 | cb(); 60 | ret.finish(); 61 | } 62 | else 63 | ret.finish(cb()); 64 | } 65 | catch (Throwable t) 66 | { 67 | ret.error(t); 68 | } 69 | }).start(); 70 | return ret; 71 | } 72 | 73 | static Future!T fromError(T)(Throwable error) 74 | { 75 | auto ret = new typeof(return); 76 | ret.error = error; 77 | ret.has = true; 78 | return ret; 79 | } 80 | 81 | static if (is(T == void)) 82 | void finish() 83 | { 84 | assert(!has); 85 | has = true; 86 | if (_onDone) 87 | _onDone(); 88 | } 89 | else 90 | void finish(T value) 91 | { 92 | assert(!has); 93 | this.value = value; 94 | has = true; 95 | if (_onDone) 96 | _onDone(); 97 | } 98 | 99 | void error(Throwable t) 100 | { 101 | assert(!has); 102 | exception = t; 103 | has = true; 104 | if (_onDone) 105 | _onDone(); 106 | } 107 | 108 | /// Waits for the result of this future using Thread.sleep 109 | T getBlocking(alias sleepDur = 1.msecs)() 110 | { 111 | while (!has) 112 | Thread.sleep(sleepDur); 113 | if (_worker) 114 | { 115 | _worker.join(); 116 | _worker = null; 117 | } 118 | if (exception) 119 | throw exception; 120 | static if (!is(T == void)) 121 | return value; 122 | } 123 | 124 | /// Waits for the result of this future using Fiber.yield 125 | T getYield() 126 | { 127 | assert(Fiber.getThis() !is null, 128 | "Attempted to getYield without being in a Fiber context"); 129 | 130 | while (!has) 131 | Fiber.yield(); 132 | if (_worker) 133 | { 134 | _worker.join(); 135 | _worker = null; 136 | } 137 | if (exception) 138 | throw exception; 139 | static if (!is(T == void)) 140 | return value; 141 | } 142 | } 143 | 144 | enum string gthreadsAsyncProxy(string call) = `auto __futureRet = new typeof(return); 145 | gthreads.create({ 146 | mixin(traceTask); 147 | try 148 | { 149 | __futureRet.finish(` ~ call ~ `); 150 | } 151 | catch (Throwable t) 152 | { 153 | __futureRet.error(t); 154 | } 155 | }); 156 | return __futureRet; 157 | `; 158 | 159 | void create(T)(TaskPool pool, T fun) if (isCallable!T) 160 | { 161 | pool.put(task(fun)); 162 | } 163 | -------------------------------------------------------------------------------- /source/workspaced/helpers.d: -------------------------------------------------------------------------------- 1 | module workspaced.helpers; 2 | 3 | import std.ascii; 4 | import std.string; 5 | 6 | string determineIndentation(scope const(char)[] code) @safe 7 | { 8 | const(char)[] indent = null; 9 | foreach (line; code.lineSplitter) 10 | { 11 | if (line.strip.length == 0) 12 | continue; 13 | indent = line[0 .. $ - line.stripLeft.length]; 14 | } 15 | return indent.idup; 16 | } 17 | 18 | int stripLineEndingLength(scope const(char)[] code) @safe @nogc 19 | { 20 | switch (code.length) 21 | { 22 | case 0: 23 | return 0; 24 | case 1: 25 | return code[0] == '\r' || code[0] == '\n' ? 1 : 0; 26 | default: 27 | if (code[$ - 2 .. $] == "\r\n") 28 | return 2; 29 | else if (code[$ - 1] == '\r' || code[$ - 1] == '\n') 30 | return 1; 31 | else 32 | return 0; 33 | } 34 | } 35 | 36 | bool isIdentifierChar(dchar c) @safe @nogc 37 | { 38 | return c.isAlphaNum || c == '_'; 39 | } 40 | 41 | ptrdiff_t indexOfKeyword(scope const(char)[] code, string keyword, ptrdiff_t start = 0) @safe @nogc 42 | { 43 | ptrdiff_t index = start; 44 | while (true) 45 | { 46 | index = code.indexOf(keyword, index); 47 | if (index == -1) 48 | break; 49 | 50 | if ((index > 0 && code[index - 1].isIdentifierChar) 51 | || (index + keyword.length < code.length && code[index + keyword.length].isIdentifierChar)) 52 | { 53 | index++; 54 | continue; 55 | } 56 | else 57 | break; 58 | } 59 | return index; 60 | } 61 | 62 | bool endsWithKeyword(scope const(char)[] code, string keyword) @safe @nogc 63 | { 64 | return code == keyword || (code.endsWith(keyword) && code[$ - 1 - keyword.length] 65 | .isIdentifierChar); 66 | } 67 | 68 | bool isIdentifierSeparatingChar(dchar c) @safe @nogc 69 | { 70 | return c < 48 || (c > 57 && c < 65) || c == '[' || c == '\\' || c == ']' 71 | || c == '`' || (c > 122 && c < 128) || c == '\u2028' || c == '\u2029'; // line separators 72 | } 73 | 74 | version (unittest) 75 | { 76 | import std.json; 77 | 78 | /// Iterates over all files in the given folder, reads them as D files until 79 | /// a __EOF__ token is encountered, then parses the following lines in this 80 | /// format per file: 81 | /// - If the line is empty or starts with `//` ignore it 82 | /// - If the line starts with `:` it's a variable assignment in form `:variable=JSON` 83 | /// - Otherwise it's a tab separated line like `1 2 3` 84 | /// Finally, it's tested that at least one test has been tested. 85 | void runTestDataFileTests(string dir, 86 | void delegate() onFileStart, 87 | void delegate(string code, string variable, JSONValue value) setVariable, 88 | void delegate(string code, string[] parts, string line) onTestLine, 89 | void delegate(string code) onFileFinished, 90 | string __file = __FILE__, 91 | size_t __line = __LINE__) 92 | { 93 | import core.exception; 94 | import std.algorithm; 95 | import std.array; 96 | import std.conv; 97 | import std.file; 98 | import std.stdio; 99 | 100 | int noTested = 0; 101 | foreach (testFile; dirEntries(dir, SpanMode.shallow)) 102 | { 103 | int lineNo = 0; 104 | try 105 | { 106 | auto testCode = appender!string; 107 | bool inCode = true; 108 | if (onFileStart) 109 | onFileStart(); 110 | foreach (line; File(testFile, "r").byLine) 111 | { 112 | lineNo++; 113 | if (line == "__EOF__") 114 | { 115 | inCode = false; 116 | continue; 117 | } 118 | 119 | if (inCode) 120 | { 121 | testCode ~= line; 122 | testCode ~= '\n'; // normalize CRLF to LF 123 | } 124 | else if (!line.length || line.startsWith("//")) 125 | { 126 | continue; 127 | } 128 | else if (line[0] == ':') 129 | { 130 | auto variable = line[1 .. $].idup.findSplit("="); 131 | if (setVariable) 132 | setVariable(testCode.data, variable[0], parseJSON(variable[2])); 133 | } 134 | else 135 | { 136 | if (onTestLine) 137 | { 138 | string lineDup = line.idup; 139 | onTestLine(testCode.data, lineDup.split("\t"), lineDup); 140 | } 141 | } 142 | } 143 | 144 | if (onFileFinished) 145 | onFileFinished(testCode.data); 146 | noTested++; 147 | } 148 | catch (AssertError e) 149 | { 150 | e.file = __file; 151 | e.line = __line; 152 | e.msg = "in " ~ testFile ~ "(" ~ lineNo.to!string ~ "): " ~ e.msg; 153 | throw e; 154 | } 155 | } 156 | 157 | assert(noTested > 0); 158 | } 159 | } 160 | -------------------------------------------------------------------------------- /source/workspaced/dub/lintgenerator.d: -------------------------------------------------------------------------------- 1 | /** 2 | Generator for direct compiler builds. 3 | 4 | Copyright: © 2013-2013 rejectedsoftware e.K. 5 | License: Subject to the terms of the MIT license. 6 | Authors: Sönke Ludwig, Jan Jurzitza 7 | */ 8 | module workspaced.dub.lintgenerator; 9 | 10 | // TODO: this sucks, this is copy pasted from build.d in dub and only removed binary output here 11 | 12 | import dub.compilers.compiler; 13 | import dub.compilers.utils; 14 | import dub.generators.generator; 15 | import dub.internal.utils; 16 | import dub.internal.vibecompat.core.file; 17 | import dub.internal.vibecompat.core.log; 18 | import dub.internal.vibecompat.inet.path; 19 | import dub.package_; 20 | import dub.packagemanager; 21 | import dub.project; 22 | 23 | import std.algorithm; 24 | import std.array; 25 | import std.conv; 26 | import std.exception; 27 | import std.file; 28 | import std.process; 29 | import std.string; 30 | import std.encoding : sanitize; 31 | 32 | version (Windows) 33 | enum objSuffix = ".obj"; 34 | else 35 | enum objSuffix = ".o"; 36 | 37 | class DubLintGenerator : ProjectGenerator 38 | { 39 | this(Project project) 40 | { 41 | super(project); 42 | } 43 | 44 | override void generateTargets(GeneratorSettings settings, in TargetInfo[string] targets) 45 | { 46 | auto dcl = settings.compiler; 47 | 48 | auto root_ti = targets[m_project.rootPackage.name]; 49 | 50 | logInfo("Performing \"%s\" build using %s for %-(%s, %).", settings.buildType, 51 | settings.platform.compilerBinary, settings.platform.architecture); 52 | 53 | buildTarget(settings, root_ti.buildSettings.dup, m_project.rootPackage, root_ti.config); 54 | } 55 | 56 | private void buildTarget(GeneratorSettings settings, 57 | BuildSettings buildsettings, in Package pack, string config) 58 | { 59 | auto cwd = NativePath(getcwd()); 60 | 61 | // make all paths relative to shrink the command line 62 | string makeRelative(string path) 63 | { 64 | return shrinkPath(NativePath(path), cwd); 65 | } 66 | 67 | foreach (ref f; buildsettings.sourceFiles) 68 | f = makeRelative(f); 69 | foreach (ref p; buildsettings.importPaths) 70 | p = makeRelative(p); 71 | foreach (ref p; buildsettings.stringImportPaths) 72 | p = makeRelative(p); 73 | 74 | // perform the actual build 75 | performDirectBuild(settings, buildsettings, pack, config); 76 | } 77 | 78 | private void performDirectBuild(GeneratorSettings settings, 79 | ref BuildSettings buildsettings, in Package pack, string config) 80 | { 81 | auto cwd = NativePath(getcwd()); 82 | 83 | // make file paths relative to shrink the command line 84 | foreach (ref f; buildsettings.sourceFiles) 85 | { 86 | auto fp = NativePath(f); 87 | if (fp.absolute) 88 | fp = fp.relativeTo(cwd); 89 | f = fp.toNativeString(); 90 | } 91 | 92 | logInfo("%s %s: building configuration \"%s\"...", pack.name, pack.version_, config); 93 | 94 | // make all target/import paths relative 95 | string makeRelative(string path) 96 | { 97 | auto p = NativePath(path); 98 | // storing in a separate temprary to work around #601 99 | auto prel = p.absolute ? p.relativeTo(cwd) : p; 100 | return prel.toNativeString(); 101 | } 102 | 103 | buildsettings.targetPath = makeRelative(buildsettings.targetPath); 104 | foreach (ref p; buildsettings.importPaths) 105 | p = makeRelative(p); 106 | foreach (ref p; buildsettings.stringImportPaths) 107 | p = makeRelative(p); 108 | 109 | buildWithCompiler(settings, buildsettings); 110 | } 111 | 112 | private void buildWithCompiler(GeneratorSettings settings, BuildSettings buildsettings) 113 | { 114 | scope (failure) 115 | { 116 | logDiagnostic("FAIL %s %s %s", buildsettings.targetPath, 117 | buildsettings.targetName, buildsettings.targetType); 118 | } 119 | 120 | buildsettings.libs = null; 121 | buildsettings.lflags = null; 122 | buildsettings.addOptions(BuildOption.syntaxOnly); 123 | buildsettings.sourceFiles = buildsettings.sourceFiles.filter!(f => !isLinkerFile(settings.platform, f)).array; 124 | 125 | settings.compiler.prepareBuildSettings(buildsettings, settings.platform, BuildSetting.commandLine); 126 | 127 | settings.compiler.invoke(buildsettings, settings.platform, settings.compileCallback); 128 | } 129 | } 130 | 131 | private string shrinkPath(NativePath path, NativePath base) 132 | { 133 | auto orig = path.toNativeString(); 134 | if (!path.absolute) 135 | return orig; 136 | auto ret = path.relativeTo(base).toNativeString(); 137 | return ret.length < orig.length ? ret : orig; 138 | } 139 | -------------------------------------------------------------------------------- /source/workspaced/com/dmd.d: -------------------------------------------------------------------------------- 1 | module workspaced.com.dmd; 2 | 3 | import core.thread; 4 | import std.array; 5 | import std.datetime; 6 | import std.datetime.stopwatch : StopWatch; 7 | import std.file; 8 | import std.json; 9 | import std.path; 10 | import std.process; 11 | import std.random; 12 | 13 | import painlessjson; 14 | 15 | import workspaced.api; 16 | 17 | @component("dmd") 18 | class DMDComponent : ComponentWrapper 19 | { 20 | mixin DefaultComponentWrapper; 21 | 22 | /// Tries to compile a snippet of code with the import paths in the current directory. The arguments `-c -o-` are implicit. 23 | /// The sync function may be used to prevent other measures from running while this is running. 24 | /// Params: 25 | /// cb = async callback 26 | /// code = small code snippet to try to compile 27 | /// dmdArguments = additional arguments to pass to dmd before file name 28 | /// count = how often to compile (duration is divided by either this or less in case timeout is reached) 29 | /// timeoutMsecs = when to abort compilation after, note that this will not abort mid-compilation but not do another iteration if this timeout has been reached. 30 | /// Returns: [DMDMeasureReturn] containing logs from only the first compilation pass 31 | Future!DMDMeasureReturn measure(scope const(char)[] code, 32 | string[] dmdArguments = [], int count = 1, int timeoutMsecs = 5000) 33 | { 34 | return typeof(return).async(() => measureSync(code, dmdArguments, count, timeoutMsecs)); 35 | } 36 | 37 | /// ditto 38 | DMDMeasureReturn measureSync(scope const(char)[] code, 39 | string[] dmdArguments = [], int count = 1, int timeoutMsecs = 5000) 40 | { 41 | dmdArguments ~= ["-c", "-o-"]; 42 | DMDMeasureReturn ret; 43 | 44 | auto timeout = timeoutMsecs.msecs; 45 | 46 | StopWatch sw; 47 | 48 | int effective; 49 | 50 | foreach (i; 0 .. count) 51 | { 52 | if (sw.peek >= timeout) 53 | break; 54 | string[] baseArgs = [path]; 55 | foreach (path; importPaths) 56 | baseArgs ~= "-I=" ~ path; 57 | foreach (path; stringImportPaths) 58 | baseArgs ~= "-J=" ~ path; 59 | auto pipes = pipeProcess(baseArgs ~ dmdArguments ~ "-", 60 | Redirect.stderrToStdout | Redirect.stdout | Redirect.stdin, null, 61 | Config.none, instance.cwd); 62 | pipes.stdin.write(code); 63 | pipes.stdin.close(); 64 | if (i == 0) 65 | { 66 | if (count == 0) 67 | sw.start(); 68 | ret.log = pipes.stdout.byLineCopy().array; 69 | auto status = pipes.pid.wait(); 70 | if (count == 0) 71 | sw.stop(); 72 | ret.success = status == 0; 73 | ret.crash = status < 0; 74 | } 75 | else 76 | { 77 | if (count < 10 || i != 1) 78 | sw.start(); 79 | pipes.pid.wait(); 80 | if (count < 10 || i != 1) 81 | sw.stop(); 82 | pipes.stdout.close(); 83 | effective++; 84 | } 85 | if (!ret.success) 86 | break; 87 | } 88 | 89 | ret.duration = sw.peek; 90 | 91 | if (effective > 0) 92 | ret.duration = ret.duration / effective; 93 | 94 | return ret; 95 | } 96 | 97 | string path() @property @ignoredFunc const 98 | { 99 | return config.get("dmd", "path", "dmd"); 100 | } 101 | } 102 | 103 | /// 104 | version (DigitalMars) unittest 105 | { 106 | scope backend = new WorkspaceD(); 107 | auto workspace = makeTemporaryTestingWorkspace; 108 | auto instance = backend.addInstance(workspace.directory); 109 | backend.register!DMDComponent; 110 | auto measure = backend.get!DMDComponent(workspace.directory) 111 | .measure("import std.stdio;", null, 100).getBlocking; 112 | assert(measure.success); 113 | assert(measure.duration < 5.seconds); 114 | } 115 | 116 | /// 117 | struct DMDMeasureReturn 118 | { 119 | /// true if dmd returned 0 120 | bool success; 121 | /// true if an ICE occured (segfault / negative return code) 122 | bool crash; 123 | /// compilation output 124 | string[] log; 125 | /// how long compilation took (serialized to msecs float in json) 126 | Duration duration; 127 | 128 | /// Converts a json object to [DMDMeasureReturn] 129 | static DMDMeasureReturn fromJSON(JSONValue value) 130 | { 131 | DMDMeasureReturn ret; 132 | if (auto success = "success" in value) 133 | ret.success = success.type == JSONType.true_; 134 | if (auto crash = "crash" in value) 135 | ret.crash = crash.type == JSONType.true_; 136 | if (auto log = "log" in value) 137 | ret.log = (*log).fromJSON!(string[]); 138 | if (auto duration = "duration" in value) 139 | ret.duration = (cast(long)(duration.floating * 10_000)).hnsecs; 140 | return ret; 141 | } 142 | 143 | /// Converts this object to a [JSONValue] 144 | JSONValue toJSON() const 145 | { 146 | //dfmt off 147 | return JSONValue([ 148 | "success": JSONValue(success), 149 | "crash": JSONValue(crash), 150 | "log": log.toJSON, 151 | "duration": JSONValue(duration.total!"hnsecs" / cast(double) 10_000) 152 | ]); 153 | //dfmt on 154 | } 155 | } 156 | -------------------------------------------------------------------------------- /source/workspaced/com/snippets/dependencies.d: -------------------------------------------------------------------------------- 1 | module workspaced.com.snippets.dependencies; 2 | 3 | import workspaced.api; 4 | import workspaced.com.dub; 5 | import workspaced.com.snippets; 6 | 7 | import std.algorithm; 8 | 9 | /// 10 | alias SnippetList = PlainSnippet[]; 11 | 12 | /// A list of dependencies usable in an associative array 13 | struct DependencySet 14 | { 15 | string[] sorted; 16 | 17 | void set(string[] deps) 18 | { 19 | deps.sort!"a &v)()); 56 | return ret; 57 | } 58 | } 59 | 60 | class DependencyBasedSnippetProvider : SnippetProvider 61 | { 62 | SnippetList[DependencySet] snippets; 63 | 64 | void addSnippet(string[] requiredDependencies, PlainSnippet snippet) 65 | { 66 | DependencySet set; 67 | set.set(requiredDependencies); 68 | 69 | if (auto v = set in snippets) 70 | *v ~= snippet; 71 | else 72 | snippets[set] = [snippet]; 73 | } 74 | 75 | Future!(Snippet[]) provideSnippets(scope const WorkspaceD.Instance instance, 76 | scope const(char)[] file, scope const(char)[] code, int position, const SnippetInfo info) 77 | { 78 | if (!instance.has!DubComponent) 79 | return typeof(return).fromResult(null); 80 | else 81 | { 82 | string id = typeid(this).name; 83 | auto dub = instance.get!DubComponent; 84 | return typeof(return).async(delegate() { 85 | string[] deps; 86 | foreach (dep; dub.dependencies) 87 | { 88 | deps ~= dep.name; 89 | deps ~= dep.dependencies.keys; 90 | } 91 | Snippet[] ret; 92 | foreach (k, v; snippets) 93 | { 94 | if (k.hasAll(deps)) 95 | { 96 | foreach (snip; v) 97 | if (snip.levels.canFind(info.level)) 98 | ret ~= snip.buildSnippet(id); 99 | } 100 | } 101 | return ret; 102 | }); 103 | } 104 | } 105 | 106 | Future!Snippet resolveSnippet(scope const WorkspaceD.Instance instance, 107 | scope const(char)[] file, scope const(char)[] code, int position, 108 | const SnippetInfo info, Snippet snippet) 109 | { 110 | snippet.resolved = true; 111 | return typeof(return).fromResult(snippet); 112 | } 113 | } 114 | 115 | unittest 116 | { 117 | DependencySet set; 118 | set.set(["vibe-d", "mir", "serve-d"]); 119 | assert(set.hasAll(["vibe-d", "serve-d", "mir"])); 120 | assert(set.hasAll(["vibe-d", "serve-d", "serve-d", "serve-d", "mir", "mir"])); 121 | assert(set.hasAll(["vibe-d", "serve-d", "mir", "workspace-d"])); 122 | assert(set.hasAll(["diet-ng", "vibe-d", "serve-d", "mir", "workspace-d"])); 123 | assert(!set.hasAll(["diet-ng", "serve-d", "mir", "workspace-d"])); 124 | assert(!set.hasAll(["diet-ng", "serve-d", "vibe-d", "workspace-d"])); 125 | assert(!set.hasAll(["diet-ng", "mir", "mir", "vibe-d", "workspace-d"])); 126 | assert(!set.hasAll(["diet-ng", "mir", "vibe-d", "workspace-d"])); 127 | 128 | set.set(["vibe-d:http"]); 129 | assert(set.hasAll([ 130 | "botan", "botan", "botan-math", "botan-math", "diet-ng", "diet-ng", 131 | "eventcore", "eventcore", "libasync", "libasync", "memutils", 132 | "memutils", "memutils", "mir-linux-kernel", "mir-linux-kernel", 133 | "openssl", "openssl", "openssl", "stdx-allocator", "stdx-allocator", 134 | "stdx-allocator", "taggedalgebraic", "taggedalgebraic", "vibe-core", 135 | "vibe-core", "vibe-d", "vibe-d:core", "vibe-d:core", "vibe-d:core", 136 | "vibe-d:core", "vibe-d:core", "vibe-d:core", "vibe-d:crypto", 137 | "vibe-d:crypto", "vibe-d:crypto", "vibe-d:data", "vibe-d:data", 138 | "vibe-d:data", "vibe-d:http", "vibe-d:http", "vibe-d:http", "vibe-d:http", 139 | "vibe-d:http", "vibe-d:inet", "vibe-d:inet", "vibe-d:inet", "vibe-d:inet", 140 | "vibe-d:mail", "vibe-d:mail", "vibe-d:mongodb", "vibe-d:mongodb", 141 | "vibe-d:redis", "vibe-d:redis", "vibe-d:stream", "vibe-d:stream", 142 | "vibe-d:stream", "vibe-d:stream", "vibe-d:textfilter", "vibe-d:textfilter", 143 | "vibe-d:textfilter", "vibe-d:textfilter", "vibe-d:tls", "vibe-d:tls", 144 | "vibe-d:tls", "vibe-d:tls", "vibe-d:utils", "vibe-d:utils", "vibe-d:utils", 145 | "vibe-d:utils", "vibe-d:utils", "vibe-d:utils", "vibe-d:web", "vibe-d:web" 146 | ])); 147 | 148 | set.set(null); 149 | assert(set.hasAll([])); 150 | assert(set.hasAll(["foo"])); 151 | } 152 | -------------------------------------------------------------------------------- /test/tc_as_a_exe/source/app.d: -------------------------------------------------------------------------------- 1 | import std.bitmanip; 2 | import std.conv; 3 | import std.file; 4 | import std.process; 5 | import std.string; 6 | import std.stdio; 7 | import std.json; 8 | 9 | version (assert) 10 | { 11 | } 12 | else 13 | static assert(false, "Compile with asserts."); 14 | 15 | void main() 16 | { 17 | string dir = getcwd; 18 | JSONValue response; 19 | 20 | //scope backend = new WorkspaceD(); 21 | version (Windows) 22 | auto backend = pipeProcess(["..\\..\\workspace-d.exe"], Redirect.stdout | Redirect.stdin); 23 | else 24 | auto backend = pipeProcess(["../../workspace-d"], Redirect.stdout | Redirect.stdin); 25 | 26 | //auto instance = backend.addInstance(dir); 27 | backend.stdin.writeRequest(10, `{"cmd": "new", "cwd": ` ~ JSONValue(dir).toString ~ `}`); 28 | assert(backend.stdout.readResponse(10).type == JSONType.true_); 29 | 30 | //backend.register!FSWorkspaceComponent; 31 | backend.stdin.writeRequest(20, 32 | `{"cmd": "load", "component": "fsworkspace", "cwd": ` ~ JSONValue(dir).toString ~ `}`); 33 | assert(backend.stdout.readResponse(20).type == JSONType.true_); 34 | 35 | //backend.register!DscannerComponent; 36 | backend.stdin.writeRequest(21, 37 | `{"cmd": "load", "component": "dscanner", "cwd": ` ~ JSONValue(dir).toString ~ `}`); 38 | assert(backend.stdout.readResponse(21).type == JSONType.true_); 39 | 40 | //auto fsworkspace = backend.get!FSWorkspaceComponent(dir); 41 | //assert(instance.importPaths == [getcwd]); 42 | backend.stdin.writeRequest(30, `{"cmd": "import-paths", "cwd": ` ~ JSONValue(dir).toString ~ `}`); 43 | response = backend.stdout.readResponse(30); 44 | assert(response.type == JSONType.array); 45 | assert(response.array.length == 1); 46 | assert(response.array[0].type == JSONType.string); 47 | assert(response.array[0].str == getcwd); 48 | 49 | //fsworkspace.addImports(["source"]); 50 | backend.stdin.writeRequest(40, 51 | `{"cmd": "call", "component": "fsworkspace", "method": "addImports", "params": [["source"]], "cwd": ` ~ JSONValue( 52 | dir).toString ~ `}`); 53 | backend.stdout.readResponse(40); 54 | 55 | //dscanner.resolveRanges(code, ref issues); 56 | backend.stdin.writeRequest(41, 57 | `{"cmd": "call", "component": "dscanner", "method": "resolveRanges", "params": ["cool code", [{"file": "something.d", "line": 1, "column": 4, "type": "custom.type", "description": "custom description", "key": "custom.key"}]], "cwd": ` ~ JSONValue( 58 | dir).toString ~ `}`); 59 | response = backend.stdout.readResponse(41); 60 | assert(response.type == JSONType.array); 61 | assert(response.array.length == 1); 62 | assert(response.array[0].type == JSONType.object); 63 | assert(response.array[0].object["file"].str == "something.d"); 64 | assert(response.array[0].object["line"].integer == 1); 65 | assert(response.array[0].object["column"].integer == 4); 66 | assert(response.array[0].object["type"].str == "custom.type"); 67 | assert(response.array[0].object["description"].str == "custom description"); 68 | assert(response.array[0].object["key"].str == "custom.key"); 69 | assert(response.array[0].object["range"].type == JSONType.array); 70 | 71 | //assert(instance.importPaths == [getcwd, "source"]); 72 | backend.stdin.writeRequest(50, `{"cmd": "import-paths", "cwd": ` ~ JSONValue(dir).toString ~ `}`); 73 | response = backend.stdout.readResponse(50); 74 | assert(response.type == JSONType.array); 75 | assert(response.array.length == 2); 76 | assert(response.array[0].type == JSONType.string); 77 | assert(response.array[0].str == getcwd); 78 | assert(response.array[1].type == JSONType.string); 79 | assert(response.array[1].str == "source"); 80 | } 81 | 82 | void writeRequest(File stdin, int id, JSONValue data) 83 | { 84 | stdin.writeRequest(id, data.toString); 85 | } 86 | 87 | void writeRequest(File stdin, int id, string data) 88 | { 89 | stdin.rawWrite((cast(uint) data.length + 4).nativeToBigEndian); 90 | stdin.rawWrite(id.nativeToBigEndian); 91 | stdin.rawWrite(data); 92 | stdin.flush(); 93 | writefln("%s >> %s", id, data); 94 | } 95 | 96 | JSONValue readResponse(File stdout, int expectedId = 0x7F000001) 97 | { 98 | import std.algorithm; 99 | 100 | ubyte[512] skipBuf; 101 | ubyte[4] intBuf; 102 | uint length; 103 | int reqId; 104 | while (true) 105 | { 106 | stdout.rawRead(intBuf[]); 107 | length = intBuf.bigEndianToNative!uint; 108 | stdout.rawRead(intBuf[]); 109 | reqId = intBuf.bigEndianToNative!uint; 110 | if (expectedId != 0x7F000001 && expectedId != reqId) 111 | { 112 | writefln("%s << ", reqId, length); 113 | if (length > 4) 114 | { 115 | length -= 4; 116 | while (length > 0) 117 | length -= stdout.rawRead(skipBuf[0 .. min($, length)]).length; 118 | } 119 | } 120 | else 121 | break; 122 | } 123 | 124 | if (length > 4) 125 | { 126 | ubyte[] data = new ubyte[length - 4]; 127 | stdout.rawRead(data); 128 | writefln("%s << %s", reqId, cast(char[]) data); 129 | return parseJSON(cast(char[]) data); 130 | } 131 | else 132 | { 133 | writefln("%s << ", reqId); 134 | return JSONValue.init; 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /source/workspaced/com/snippets/smart.d: -------------------------------------------------------------------------------- 1 | module workspaced.com.snippets.smart; 2 | 3 | // debug = SnippetScope; 4 | 5 | import workspaced.api; 6 | import workspaced.com.snippets; 7 | 8 | import std.algorithm; 9 | import std.conv; 10 | import std.string; 11 | 12 | class SmartSnippetProvider : SnippetProvider 13 | { 14 | Future!(Snippet[]) provideSnippets(scope const WorkspaceD.Instance instance, 15 | scope const(char)[] file, scope const(char)[] code, int position, const SnippetInfo info) 16 | { 17 | Snippet[] res; 18 | 19 | if (info.loopScope.supported) 20 | { 21 | if (info.loopScope.numItems > 1) 22 | { 23 | res ~= ndForeach(info.loopScope.numItems, info.loopScope.iterator); 24 | res ~= simpleForeach(); 25 | res ~= stringIterators(); 26 | } 27 | else if (info.loopScope.stringIterator) 28 | { 29 | res ~= simpleForeach(); 30 | res ~= stringIterators(info.loopScope.iterator); 31 | } 32 | else 33 | { 34 | res ~= simpleForeach(info.loopScope.iterator, info.loopScope.type); 35 | res ~= stringIterators(); 36 | } 37 | } 38 | 39 | if (info.lastStatement.type == "IfStatement" 40 | && !info.lastStatement.ifHasElse) 41 | { 42 | int ifIndex = info.contextTokenIndex == 0 ? position : info.contextTokenIndex; 43 | auto hasBraces = code[0 .. max(min(ifIndex, $), 0)].stripRight.endsWith("}"); 44 | Snippet snp; 45 | snp.providerId = typeid(this).name; 46 | snp.id = "else"; 47 | snp.title = "else"; 48 | snp.shortcut = "else"; 49 | snp.documentation = "else block"; 50 | if (hasBraces) 51 | { 52 | snp.plain = "else {\n\t\n}"; 53 | snp.snippet = "else {\n\t$0\n}"; 54 | } 55 | else 56 | { 57 | snp.plain = "else\n\t"; 58 | snp.snippet = "else\n\t$0"; 59 | } 60 | snp.unformatted = true; 61 | snp.resolved = true; 62 | res ~= snp; 63 | } 64 | 65 | debug (SnippetScope) 66 | { 67 | import painlessjson : toJSON; 68 | 69 | Snippet ret; 70 | ret.providerId = typeid(this).name; 71 | ret.id = "workspaced-snippet-debug"; 72 | ret.title = "[DEBUG] Snippet"; 73 | ret.shortcut = "__debug_snippet"; 74 | ret.plain = ret.snippet = info.toJSON.toPrettyString; 75 | ret.unformatted = true; 76 | ret.resolved = true; 77 | res ~= ret; 78 | } 79 | 80 | return typeof(return).fromResult(res.length ? res : null); 81 | } 82 | 83 | Future!Snippet resolveSnippet(scope const WorkspaceD.Instance instance, 84 | scope const(char)[] file, scope const(char)[] code, int position, 85 | const SnippetInfo info, Snippet snippet) 86 | { 87 | return typeof(return).fromResult(snippet); 88 | } 89 | 90 | Snippet ndForeach(int n, string name = null) 91 | { 92 | Snippet ret; 93 | ret.providerId = typeid(this).name; 94 | ret.id = "nd"; 95 | ret.title = "foreach over " ~ n.to!string ~ " keys"; 96 | if (name.length) 97 | ret.title ~= " (over " ~ name ~ ")"; 98 | ret.shortcut = "foreach"; 99 | ret.documentation = "Foreach over locally defined variable with " ~ n.to!string ~ " keys."; 100 | string keys; 101 | if (n == 2) 102 | { 103 | keys = "key, value"; 104 | } 105 | else if (n <= 4) 106 | { 107 | foreach (i; 0 .. n - 1) 108 | { 109 | keys ~= cast(char)('i' + i) ~ ", "; 110 | } 111 | keys ~= "value"; 112 | } 113 | else 114 | { 115 | foreach (i; 0 .. n - 1) 116 | { 117 | keys ~= "k" ~ (i + 1).to!string ~ ", "; 118 | } 119 | keys ~= "value"; 120 | } 121 | 122 | if (name.length) 123 | { 124 | ret.plain = "foreach (" ~ keys ~ "; " ~ name ~ ") {\n\t\n}"; 125 | ret.snippet = "foreach (${1:" ~ keys ~ "}; ${2:" ~ name ~ "}) {\n\t$0\n}"; 126 | } 127 | else 128 | { 129 | ret.plain = "foreach (" ~ keys ~ "; map) {\n\t\n}"; 130 | ret.snippet = "foreach (${1:" ~ keys ~ "}; ${2:map}) {\n\t$0\n}"; 131 | } 132 | ret.resolved = true; 133 | return ret; 134 | } 135 | 136 | Snippet simpleForeach(string name = null, string type = null) 137 | { 138 | Snippet ret; 139 | ret.providerId = typeid(this).name; 140 | ret.id = "simple"; 141 | ret.title = "foreach loop"; 142 | if (name.length) 143 | ret.title ~= " (over " ~ name ~ ")"; 144 | ret.shortcut = "foreach"; 145 | ret.documentation = name.length 146 | ? "Foreach over locally defined variable." : "Foreach over a variable or range."; 147 | string t = type.length ? type ~ " " : null; 148 | if (name.length) 149 | { 150 | ret.plain = "foreach (" ~ t ~ "key; " ~ name ~ ") {\n\t\n}"; 151 | ret.snippet = "foreach (" ~ t ~ "${1:key}; ${2:" ~ name ~ "}) {\n\t$0\n}"; 152 | } 153 | else 154 | { 155 | ret.plain = "foreach (" ~ t ~ "key; list) {\n\t\n}"; 156 | ret.snippet = "foreach (" ~ t ~ "${1:key}; ${2:list}) {\n\t$0\n}"; 157 | } 158 | ret.resolved = true; 159 | return ret; 160 | } 161 | 162 | Snippet stringIterators(string name = null) 163 | { 164 | Snippet ret; 165 | ret.providerId = typeid(this).name; 166 | ret.id = "str"; 167 | ret.title = "foreach loop"; 168 | if (name.length) 169 | ret.title ~= " (unicode over " ~ name ~ ")"; 170 | else 171 | ret.title ~= " (unicode)"; 172 | ret.shortcut = "foreach_utf"; 173 | ret.documentation = name.length 174 | ? "Foreach over locally defined variable." : "Foreach over a variable or range."; 175 | if (name.length) 176 | { 177 | ret.plain = "foreach (char key; " ~ name ~ ") {\n\t\n}"; 178 | ret.snippet = "foreach (${1|char,wchar,dchar|} ${2:key}; ${3:" ~ name ~ "}) {\n\t$0\n}"; 179 | } 180 | else 181 | { 182 | ret.plain = "foreach (char key; str) {\n\t\n}"; 183 | ret.snippet = "foreach (${1|char,wchar,dchar|} ${2:key}; ${3:str}) {\n\t$0\n}"; 184 | } 185 | ret.resolved = true; 186 | return ret; 187 | } 188 | } 189 | -------------------------------------------------------------------------------- /test/tc_implement_interface/source/app.d: -------------------------------------------------------------------------------- 1 | import std.algorithm; 2 | import std.conv; 3 | import std.file; 4 | import std.stdio; 5 | import std.string; 6 | import std.process; 7 | 8 | import workspaced.api; 9 | import workspaced.coms; 10 | 11 | int main(string[] args) 12 | { 13 | string dir = getcwd; 14 | scope backend = new WorkspaceD(); 15 | auto instance = backend.addInstance(dir); 16 | backend.register!FSWorkspaceComponent; 17 | backend.register!DCDComponent(false); 18 | backend.register!DCDExtComponent; 19 | 20 | version (Windows) 21 | { 22 | if (exists("dcd-client.exe")) 23 | backend.globalConfiguration.set("dcd", "clientPath", "dcd-client.exe"); 24 | 25 | if (exists("dcd-server.exe")) 26 | backend.globalConfiguration.set("dcd", "serverPath", "dcd-server.exe"); 27 | } 28 | else 29 | { 30 | if (exists("dcd-client")) 31 | backend.globalConfiguration.set("dcd", "clientPath", "dcd-client"); 32 | 33 | if (exists("dcd-server")) 34 | backend.globalConfiguration.set("dcd", "serverPath", "dcd-server"); 35 | } 36 | 37 | bool verbose = args.length > 1 && (args[1] == "-v" || args[1] == "--v" || args[1] == "--verbose"); 38 | 39 | assert(backend.attachSilent(instance, "dcd"), "failed to attach DCD, is it not installed correctly?"); 40 | 41 | auto fsworkspace = backend.get!FSWorkspaceComponent(dir); 42 | auto dcd = backend.get!DCDComponent(dir); 43 | auto dcdext = backend.get!DCDExtComponent(dir); 44 | 45 | fsworkspace.addImports(["source"]); 46 | 47 | auto port = dcd.findAndSelectPort(cast(ushort) 9166).getBlocking; 48 | instance.config.set("dcd", "port", cast(int) port); 49 | 50 | dcd.setupServer([], true); 51 | scope (exit) 52 | { 53 | dcd.stopServerSync(); 54 | backend.shutdown(); 55 | } 56 | 57 | stderr.writeln("DCD client version: ", dcd.clientInstalledVersion); 58 | 59 | int status = 0; 60 | 61 | foreach (test; dirEntries("tests", SpanMode.shallow)) 62 | { 63 | if (!test.name.endsWith(".d")) 64 | continue; 65 | auto expect = test ~ ".expected"; 66 | auto actualFile = test ~ ".actual"; 67 | if (!expect.exists) 68 | { 69 | stderr.writeln("Warning: tests/", expect, " does not exist!"); 70 | continue; 71 | } 72 | auto source = test.readText; 73 | auto reader = File(expect).byLine; 74 | auto writer = File(actualFile, "w"); 75 | auto cmd = reader.front.splitter; 76 | string code, message; 77 | bool success; 78 | if (cmd.front == "implement") 79 | { 80 | writer.writeln("# ", reader.front); 81 | 82 | cmd.popFront; 83 | auto cmdLine = cmd.front; 84 | code = dcdext.implement(source, cmdLine.parse!uint, false).getBlocking; 85 | reader.popFront; 86 | 87 | writer.writeln(code); 88 | writer.writeln(); 89 | writer.writeln(); 90 | 91 | if (verbose) 92 | stderr.writeln(test, ": ", code); 93 | 94 | success = true; 95 | size_t index; 96 | foreach (line; reader) 97 | { 98 | if (line.startsWith("--- ") || !line.length) 99 | continue; 100 | 101 | if (line.startsWith("!")) 102 | { 103 | if (code.indexOf(line[1 .. $], index) != -1) 104 | { 105 | writer.writeln(line, " - FAIL"); 106 | success = false; 107 | message = "Did not expect to find line " ~ line[1 .. $].idup 108 | ~ " in (after " ~ index.to!string ~ " bytes) code " ~ code[index .. $]; 109 | } 110 | } 111 | else if (line.startsWith("#")) 112 | { 113 | // count occurences 114 | line = line[1 .. $]; 115 | char op = line[0]; 116 | if (!op.among!('<', '=', '>')) 117 | throw new Exception("Malformed count line: " ~ line.idup); 118 | line = line[1 .. $]; 119 | int expected = line.parse!uint; 120 | line = line[1 .. $]; 121 | int actual = countText(code[index .. $], line); 122 | bool match; 123 | if (op == '<') 124 | match = actual < expected; 125 | else if (op == '=') 126 | match = actual == expected; 127 | else if (op == '>') 128 | match = actual > expected; 129 | else 130 | assert(false); 131 | if (!match) 132 | { 133 | writer.writeln(line, " - FAIL"); 134 | success = false; 135 | message = "Expected to find the string '" ~ line.idup ~ "' " ~ op ~ " " ~ expected.to!string 136 | ~ " times but actually found it " ~ actual.to!string 137 | ~ " times (after " ~ index.to!string ~ " bytes) code " ~ code[index .. $]; 138 | } 139 | } 140 | else 141 | { 142 | bool freeze = false; 143 | if (line.startsWith(".")) 144 | { 145 | freeze = true; 146 | line = line[1 .. $]; 147 | } 148 | auto pos = code.indexOf(line, index); 149 | if (pos == -1) 150 | { 151 | writer.writeln(line, " - FAIL"); 152 | success = false; 153 | message = "Could not find " ~ line.idup ~ " in remaining (after " 154 | ~ index.to!string ~ " bytes) code " ~ code[index .. $]; 155 | } 156 | else if (!freeze) 157 | { 158 | index = pos + line.length; 159 | } 160 | } 161 | } 162 | } 163 | else if (cmd.front == "failimplement") 164 | { 165 | writer.writeln("# ", reader.front); 166 | 167 | cmd.popFront; 168 | auto cmdLine = cmd.front; 169 | code = dcdext.implement(source, cmdLine.parse!uint, false).getBlocking; 170 | if (code.length != 0) 171 | { 172 | writer.writeln("unexpected: ", code); 173 | writer.writeln(); 174 | message = "Code: " ~ code; 175 | success = false; 176 | } 177 | else 178 | { 179 | writer.write("ok\n\n"); 180 | success = true; 181 | } 182 | } 183 | else 184 | throw new Exception("Unknown command in " ~ expect ~ ": " ~ reader.front.idup); 185 | 186 | if (success) 187 | { 188 | writer.close(); 189 | std.file.remove(actualFile); 190 | writeln("Pass ", expect); 191 | } 192 | else 193 | { 194 | writer.writeln("-----------------\n\nTest failed\n\n-----------------\n\n"); 195 | writer.writeln(message); 196 | writeln("Expected fail in ", expect, " but it succeeded. ", message); 197 | status = 1; 198 | } 199 | } 200 | 201 | return status; 202 | } 203 | 204 | int countText(in char[] text, in char[] search) 205 | { 206 | int num = 0; 207 | ptrdiff_t index = text.indexOf(search); 208 | while (index != -1) 209 | { 210 | num++; 211 | index = text.indexOf(search, index + search.length); 212 | } 213 | return num; 214 | } 215 | -------------------------------------------------------------------------------- /source/workspaced/visitors/classifier.d: -------------------------------------------------------------------------------- 1 | /// Visitor classifying types and groups of regions of root definitions. 2 | module workspaced.visitors.classifier; 3 | 4 | import workspaced.visitors.attributes; 5 | 6 | import workspaced.helpers : determineIndentation; 7 | 8 | import workspaced.com.dcdext; 9 | 10 | import std.algorithm; 11 | import std.ascii; 12 | import std.meta; 13 | import std.range; 14 | import std.string; 15 | 16 | import dparse.ast; 17 | import dparse.lexer; 18 | 19 | class CodeDefinitionClassifier : AttributesVisitor 20 | { 21 | struct Region 22 | { 23 | CodeRegionType type; 24 | CodeRegionProtection protection; 25 | CodeRegionStatic staticness; 26 | string minIndentation; 27 | uint[2] region; 28 | bool affectsFollowing; 29 | 30 | bool sameBlockAs(in Region other) 31 | { 32 | return type == other.type && protection == other.protection && staticness == other.staticness; 33 | } 34 | } 35 | 36 | this(const(char)[] code) 37 | { 38 | this.code = code; 39 | } 40 | 41 | override void visit(const AliasDeclaration aliasDecl) 42 | { 43 | putRegion(CodeRegionType.aliases); 44 | } 45 | 46 | override void visit(const AliasThisDeclaration aliasDecl) 47 | { 48 | putRegion(CodeRegionType.aliases); 49 | } 50 | 51 | override void visit(const ClassDeclaration typeDecl) 52 | { 53 | putRegion(CodeRegionType.types); 54 | } 55 | 56 | override void visit(const InterfaceDeclaration typeDecl) 57 | { 58 | putRegion(CodeRegionType.types); 59 | } 60 | 61 | override void visit(const StructDeclaration typeDecl) 62 | { 63 | putRegion(CodeRegionType.types); 64 | } 65 | 66 | override void visit(const UnionDeclaration typeDecl) 67 | { 68 | putRegion(CodeRegionType.types); 69 | } 70 | 71 | override void visit(const EnumDeclaration typeDecl) 72 | { 73 | putRegion(CodeRegionType.types); 74 | } 75 | 76 | override void visit(const AnonymousEnumDeclaration typeDecl) 77 | { 78 | putRegion(CodeRegionType.types); 79 | } 80 | 81 | override void visit(const AutoDeclaration field) 82 | { 83 | putRegion(CodeRegionType.fields); 84 | } 85 | 86 | override void visit(const VariableDeclaration field) 87 | { 88 | putRegion(CodeRegionType.fields); 89 | } 90 | 91 | override void visit(const Constructor ctor) 92 | { 93 | putRegion(CodeRegionType.ctor); 94 | } 95 | 96 | override void visit(const StaticConstructor ctor) 97 | { 98 | putRegion(CodeRegionType.ctor); 99 | } 100 | 101 | override void visit(const SharedStaticConstructor ctor) 102 | { 103 | putRegion(CodeRegionType.ctor); 104 | } 105 | 106 | override void visit(const Postblit copyctor) 107 | { 108 | putRegion(CodeRegionType.copyctor); 109 | } 110 | 111 | override void visit(const Destructor dtor) 112 | { 113 | putRegion(CodeRegionType.dtor); 114 | } 115 | 116 | override void visit(const StaticDestructor dtor) 117 | { 118 | putRegion(CodeRegionType.dtor); 119 | } 120 | 121 | override void visit(const SharedStaticDestructor dtor) 122 | { 123 | putRegion(CodeRegionType.dtor); 124 | } 125 | 126 | override void visit(const FunctionDeclaration method) 127 | { 128 | putRegion((method.attributes && method.attributes.any!(a => a.atAttribute 129 | && a.atAttribute.identifier.text == "property")) ? CodeRegionType.properties 130 | : CodeRegionType.methods); 131 | } 132 | 133 | override void visit(const Declaration dec) 134 | { 135 | writtenRegion = false; 136 | currentRange = [ 137 | cast(uint) dec.tokens[0].index, 138 | cast(uint)(dec.tokens[$ - 1].index + dec.tokens[$ - 1].text.length + 1) 139 | ]; 140 | super.visit(dec); 141 | if (writtenRegion && regions.length >= 2 && regions[$ - 2].sameBlockAs(regions[$ - 1])) 142 | { 143 | auto range = regions[$ - 1].region; 144 | if (regions[$ - 1].minIndentation.scoreIndent < regions[$ - 2].minIndentation.scoreIndent) 145 | regions[$ - 2].minIndentation = regions[$ - 1].minIndentation; 146 | regions[$ - 2].region[1] = range[1]; 147 | regions.length--; 148 | } 149 | } 150 | 151 | override void visit(const AttributeDeclaration dec) 152 | { 153 | auto before = context.attributes[]; 154 | dec.accept(this); 155 | auto now = context.attributes; 156 | if (now.length > before.length) 157 | { 158 | auto permaAdded = now[before.length .. $]; 159 | 160 | } 161 | } 162 | 163 | void putRegion(CodeRegionType type, uint[2] range = typeof(uint.init)[2].init) 164 | { 165 | if (range == typeof(uint.init)[2].init) 166 | range = currentRange; 167 | 168 | CodeRegionProtection protection; 169 | CodeRegionStatic staticness; 170 | 171 | auto prot = context.protectionAttribute; 172 | bool stickyProtection = false; 173 | if (prot) 174 | { 175 | stickyProtection = prot.sticky; 176 | if (prot.attributes[0].type == tok!"private") 177 | protection = CodeRegionProtection.private_; 178 | else if (prot.attributes[0].type == tok!"protected") 179 | protection = CodeRegionProtection.protected_; 180 | else if (prot.attributes[0].type == tok!"package") 181 | { 182 | if (prot.attributes.length > 1) 183 | protection = CodeRegionProtection.packageIdentifier; 184 | else 185 | protection = CodeRegionProtection.package_; 186 | } 187 | else if (prot.attributes[0].type == tok!"public") 188 | protection = CodeRegionProtection.public_; 189 | } 190 | 191 | staticness = context.isStatic ? CodeRegionStatic.static_ : CodeRegionStatic.instanced; 192 | 193 | if (stickyProtection) 194 | { 195 | assert(prot); 196 | //dfmt off 197 | Region pr = { 198 | type: cast(CodeRegionType)0, 199 | protection: protection, 200 | staticness: cast(CodeRegionStatic)0, 201 | region: [cast(uint) prot.attributes[0].index, cast(uint) prot.attributes[0].index], 202 | affectsFollowing: true 203 | }; 204 | //dfmt on 205 | regions ~= pr; 206 | } 207 | 208 | //dfmt off 209 | Region r = { 210 | type: type, 211 | protection: protection, 212 | staticness: staticness, 213 | minIndentation: determineIndentation(code[range[0] .. range[1]]), 214 | region: range 215 | }; 216 | //dfmt on 217 | regions ~= r; 218 | writtenRegion = true; 219 | } 220 | 221 | alias visit = AttributesVisitor.visit; 222 | 223 | bool writtenRegion; 224 | const(char)[] code; 225 | Region[] regions; 226 | uint[2] currentRange; 227 | } 228 | 229 | private int scoreIndent(string indent) 230 | { 231 | auto len = indent.countUntil!(a => !a.isWhite); 232 | if (len == -1) 233 | return cast(int) indent.length; 234 | return indent[0 .. len].map!(a => a == ' ' ? 1 : 4).sum; 235 | } 236 | -------------------------------------------------------------------------------- /installer/source/app.d: -------------------------------------------------------------------------------- 1 | import std.stdio; 2 | import std.process; 3 | import std.string; 4 | import std.file; 5 | import std.path; 6 | import std.datetime; 7 | import std.conv; 8 | import std.algorithm; 9 | 10 | string tmp; 11 | 12 | int proc(string[] args, string cwd) 13 | { 14 | writeln("$ ", args.join(" ")); 15 | return spawnProcess(args, stdin, stdout, stderr, null, Config.none, cwd).wait != 0; 16 | } 17 | 18 | version (Windows) string getLDC() 19 | { 20 | try 21 | { 22 | execute(["ldc2", "--version"]); 23 | return "ldc2"; 24 | } 25 | catch (ProcessException) 26 | { 27 | try 28 | { 29 | execute(["ldc", "--version"]); 30 | return "ldc"; 31 | } 32 | catch (ProcessException) 33 | { 34 | return ""; 35 | } 36 | } 37 | } 38 | 39 | bool dubInstall(bool isClonedAlready = false, bool fetchMaster = false)(string folder, string git, string[] output, 40 | string[][] compilation = [["dub", "upgrade"], ["dub", "build", "--build=release"]]) 41 | { 42 | static if (isClonedAlready) 43 | { 44 | writeln("Using existing git repository for " ~ folder); 45 | string cwd = git; 46 | if (proc(["git", "submodule", "update", "--init", "--recursive"], cwd) != 0) 47 | { 48 | writeln("Error while cloning subpackages of " ~ folder ~ "."); 49 | return false; 50 | } 51 | } 52 | else 53 | { 54 | writeln("Cloning " ~ folder ~ " into ", tmp); 55 | if (proc(["git", "clone", "-q", "--recursive", git, folder], tmp) != 0) 56 | { 57 | writeln("Error while cloning " ~ folder ~ "."); 58 | return false; 59 | } 60 | string cwd = buildNormalizedPath(tmp, folder); 61 | static if (fetchMaster) 62 | { 63 | writeln("Using ~master for building."); 64 | } 65 | else 66 | { 67 | string tag = execute(["git", "describe", "--abbrev=0", "--tags"], null, 68 | Config.none, size_t.max, cwd).output.strip(); 69 | if (tag.canFind(" ")) 70 | { 71 | writeln("Invalid tag in git repository."); 72 | return false; 73 | } 74 | writeln("Checking out ", tag); 75 | if (proc(["git", "checkout", "-q", tag], cwd) != 0) 76 | { 77 | writeln("Error while checking out " ~ folder ~ "."); 78 | return false; 79 | } 80 | } 81 | } 82 | writeln("Compiling..."); 83 | foreach (args; compilation) 84 | if (proc(args, cwd) != 0) 85 | { 86 | writeln("Error while compiling " ~ folder ~ "."); 87 | return false; 88 | } 89 | foreach (bin; output) 90 | { 91 | auto dest = buildNormalizedPath("bin", bin.baseName); 92 | copy(buildNormalizedPath(cwd, bin), dest); 93 | version (Posix) 94 | dest.setAttributes(dest.getAttributes | octal!111); 95 | } 96 | writeln("Successfully compiled " ~ folder ~ "!"); 97 | return true; 98 | } 99 | 100 | int main(string[] args) 101 | { 102 | if (!exists("bin")) 103 | mkdir("bin"); 104 | if (isFile("bin")) 105 | { 106 | writeln("Could not initialize, bin is a file!"); 107 | writeln("Please delete bin!"); 108 | return 1; 109 | } 110 | 111 | tmp = buildNormalizedPath(tempDir, "workspaced-install-" ~ Clock.currStdTime().to!string); 112 | mkdirRecurse(tmp); 113 | 114 | writeln("Welcome to the workspace-d installation guide."); 115 | writeln("Make sure, you have dub and git installed."); 116 | string winCompiler; 117 | version (Windows) 118 | { 119 | writeln(); 120 | writeln("LDC is required on your platform!"); 121 | winCompiler = getLDC(); 122 | if (!winCompiler.length) 123 | { 124 | writeln( 125 | "WARNING: LDC could was not detected. Before submitting an issue, make sure `dub build --compiler=ldc` works!"); 126 | winCompiler = "ldc"; 127 | } 128 | } 129 | writeln(); 130 | string workspacedPath = ""; 131 | string selection; 132 | if (args.length > 1) 133 | { 134 | if (args[1].exists && isDir(args[1])) 135 | { 136 | workspacedPath = args[1]; 137 | goto SelectComponents; 138 | } 139 | else 140 | selection = args[1]; 141 | } 142 | else 143 | { 144 | SelectComponents: 145 | writeln("Which optional dependencies do you want to install?"); 146 | writeln("[1] DCD - auto completion"); 147 | writeln("[2] DScanner - code linting"); 148 | writeln("[3] dfmt - code formatting"); 149 | writeln("Enter a comma separated list of numbers"); 150 | write("Selected [all]: "); 151 | selection = readln(); 152 | if (!selection.strip().length) 153 | selection = "all"; 154 | } 155 | string[] coms = selection.split(','); 156 | bool dcd, dscanner, dfmt; 157 | foreach (com; coms) 158 | { 159 | com = com.strip().toLower(); 160 | if (com == "") 161 | continue; 162 | switch (com) 163 | { 164 | case "1": 165 | dcd = true; 166 | break; 167 | case "2": 168 | dscanner = true; 169 | break; 170 | case "3": 171 | dfmt = true; 172 | break; 173 | case "all": 174 | dcd = dscanner = dfmt = true; 175 | break; 176 | default: 177 | writeln("Component out of range, aborting. (", com, ")"); 178 | return 1; 179 | } 180 | } 181 | version (Windows) 182 | { 183 | if (workspacedPath.length) 184 | { 185 | if (!dubInstall!true("workspace-d", workspacedPath, [".\\workspace-d.exe", ".\\libcurl.dll", 186 | ".\\libeay32.dll", "ssleay32.dll"], [["dub", "upgrade"], ["dub", 187 | "build", "--compiler=" ~ winCompiler, "--combined", "--build=release"]])) // must be --combined to avoid windows issues 188 | return 1; 189 | } 190 | else if (!dubInstall("workspace-d", "https://github.com/Pure-D/workspace-d.git", 191 | [".\\workspace-d.exe", ".\\libcurl.dll", 192 | ".\\libeay32.dll", "ssleay32.dll"], [["git", "submodule", "update", 193 | "--init", "--recursive"], ["dub", "upgrade"], 194 | ["dub", "build", "--compiler=" ~ winCompiler, "--combined", "--build=release"]])) 195 | return 1; 196 | if (dcd && !dubInstall!(false, true)("DCD", "https://github.com/dlang-community/DCD.git", 197 | [".\\dcd-client.exe", ".\\dcd-server.exe"], [["dub", "upgrade"], ["dub", "build", "--build=release", 198 | "--config=client"], ["dub", "build", "--build=release", "--config=server"]])) 199 | return 1; 200 | if (dscanner && !dubInstall("Dscanner", "https://github.com/dlang-community/Dscanner.git", 201 | [".\\bin\\dscanner.exe"], [["git", "submodule", "update", "--init", 202 | "--recursive"], ["cmd", "/c", "build.bat"]])) 203 | return 1; 204 | if (dfmt && !dubInstall("dfmt", "https://github.com/dlang-community/dfmt.git", [".\\dfmt.exe"])) 205 | return 1; 206 | } 207 | else 208 | { 209 | if (workspacedPath.length) 210 | { 211 | if (!dubInstall!true("workspace-d", workspacedPath, ["./workspace-d"])) 212 | return 1; 213 | } 214 | else if (!dubInstall("workspace-d", 215 | "https://github.com/Pure-D/workspace-d.git", ["./workspace-d"])) 216 | return 1; 217 | if (dcd && !dubInstall!(false, true)("DCD", "https://github.com/dlang-community/DCD.git", 218 | ["./dcd-client", "./dcd-server"], [["dub", "upgrade"], ["dub", "build", "--build=release", 219 | "--config=client"], ["dub", "build", "--build=release", "--config=server"]])) 220 | return 1; 221 | if (dscanner && !dubInstall("Dscanner", "https://github.com/dlang-community/Dscanner.git", 222 | ["./bin/dscanner"], [["git", "submodule", "update", "--init", "--recursive"], ["make"]])) 223 | return 1; 224 | if (dfmt && !dubInstall("dfmt", "https://github.com/dlang-community/dfmt.git", ["./dfmt"])) 225 | return 1; 226 | } 227 | writeln(); 228 | writeln("SUCCESS"); 229 | writeln("Written applications to bin/"); 230 | writeln("Please add them to your PATH or modify your editor config"); 231 | return 0; 232 | } 233 | -------------------------------------------------------------------------------- /source/workspaced/com/ccdb.d: -------------------------------------------------------------------------------- 1 | /// Workspace-d component that provide import paths and errors from a 2 | /// compile_commands.json file generated by a build system. 3 | /// See https://clang.llvm.org/docs/JSONCompilationDatabase.html 4 | module workspaced.com.ccdb; 5 | 6 | import std.exception; 7 | import std.file; 8 | import std.json; 9 | import std.stdio; 10 | 11 | import workspaced.api; 12 | 13 | import containers.hashset; 14 | 15 | import dub.internal.vibecompat.core.log; 16 | 17 | @component("ccdb") 18 | class ClangCompilationDatabaseComponent : ComponentWrapper 19 | { 20 | mixin DefaultComponentWrapper; 21 | 22 | protected void load() 23 | { 24 | logDebug("loading ccdb component"); 25 | 26 | if (config.get!bool("ccdb", "registerImportProvider", true)) 27 | importPathProvider = &imports; 28 | if (config.get!bool("ccdb", "registerStringImportProvider", true)) 29 | stringImportPathProvider = &stringImports; 30 | if (config.get!bool("ccdb", "registerImportFilesProvider", false)) 31 | importFilesProvider = &fileImports; 32 | if (config.get!bool("ccdb", "registerProjectVersionsProvider", true)) 33 | projectVersionsProvider = &versions; 34 | if (config.get!bool("ccdb", "registerDebugSpecificationsProvider", true)) 35 | debugSpecificationsProvider = &debugVersions; 36 | 37 | try 38 | { 39 | if (config.get!string("ccdb", null)) 40 | { 41 | const dbPath = config.get!string("ccdb", "dbPath"); 42 | if (!dbPath) 43 | { 44 | throw new Exception("ccdb.dbPath is not provided"); 45 | } 46 | loadDb(dbPath); 47 | } 48 | } 49 | catch (Exception e) 50 | { 51 | stderr.writeln("Clang-DB Error (ignored): ", e); 52 | } 53 | } 54 | 55 | private void loadDb(string filename) 56 | { 57 | import std.algorithm : each, filter, map; 58 | import std.array : array; 59 | 60 | string jsonString = cast(string) assumeUnique(read(filename)); 61 | auto json = parseJSON(jsonString); 62 | // clang db can be quite large (e.g. 100 k lines of JSON data on large projects) 63 | // we release memory when possible to avoid having at the same time more than 64 | // two represention of the same data 65 | jsonString = null; 66 | 67 | HashSet!string imports; 68 | HashSet!string stringImports; 69 | HashSet!string fileImports; 70 | HashSet!string versions; 71 | HashSet!string debugVersions; 72 | 73 | json.array 74 | .map!(jv => CompileCommand.fromJson(jv)) 75 | .filter!(cc => cc.isValid) 76 | .each!(cc => 77 | cc.feedOptions(imports, stringImports, fileImports, versions, debugVersions) 78 | ); 79 | 80 | _importPaths = imports[].array; 81 | _stringImportPaths = stringImports[].array; 82 | _importFiles = fileImports[].array; 83 | _versions = versions[].array; 84 | _debugVersions = debugVersions[].array; 85 | } 86 | 87 | /// Lists all import paths 88 | string[] imports() @property nothrow 89 | { 90 | return _importPaths; 91 | } 92 | 93 | /// Lists all string import paths 94 | string[] stringImports() @property nothrow 95 | { 96 | return _stringImportPaths; 97 | } 98 | 99 | /// Lists all import paths to files 100 | string[] fileImports() @property nothrow 101 | { 102 | return _importFiles; 103 | } 104 | 105 | /// Lists the currently defined versions 106 | string[] versions() @property nothrow 107 | { 108 | return _versions; 109 | } 110 | 111 | /// Lists the currently defined debug versions (debug specifications) 112 | string[] debugVersions() @property nothrow 113 | { 114 | return _debugVersions; 115 | } 116 | 117 | private: 118 | 119 | string[] _importPaths, _stringImportPaths, _importFiles, _versions, _debugVersions; 120 | } 121 | 122 | private struct CompileCommand 123 | { 124 | string directory; 125 | string file; 126 | string[] args; 127 | string output; 128 | 129 | static CompileCommand fromJson(JSONValue json) 130 | { 131 | import std.algorithm : map; 132 | import std.array : array; 133 | 134 | CompileCommand cc; 135 | 136 | cc.directory = json["directory"].str; 137 | cc.file = json["file"].str; 138 | 139 | if (auto args = "arguments" in json) 140 | { 141 | cc.args = args.array.map!(jv => jv.str).array; 142 | } 143 | else if (auto cmd = "command" in json) 144 | { 145 | cc.args = unescapeCommand(cmd.str); 146 | } 147 | else 148 | { 149 | throw new Exception( 150 | "Either 'arguments' or 'command' missing from Clang compilation database"); 151 | } 152 | 153 | if (auto o = "output" in json) 154 | { 155 | cc.output = o.str; 156 | } 157 | 158 | return cc; 159 | } 160 | 161 | @property bool isValid() const 162 | { 163 | import std.algorithm : endsWith; 164 | 165 | if (args.length <= 1) 166 | return false; 167 | if (!file.endsWith(".d")) 168 | return false; 169 | return true; 170 | } 171 | 172 | void feedOptions( 173 | ref HashSet!string imports, 174 | ref HashSet!string stringImports, 175 | ref HashSet!string fileImports, 176 | ref HashSet!string versions, 177 | ref HashSet!string debugVersions) 178 | { 179 | import std.algorithm : startsWith; 180 | 181 | enum importMark = "-I"; // optional = 182 | enum stringImportMark = "-J"; // optional = 183 | enum fileImportMark = "-i="; 184 | enum versionMark = "-version="; 185 | enum debugMark = "-debug="; 186 | 187 | foreach (arg; args) 188 | { 189 | const mark = arg.startsWith( 190 | importMark, stringImportMark, fileImportMark, versionMark, debugMark 191 | ); 192 | 193 | switch (mark) 194 | { 195 | case 0: 196 | break; 197 | case 1: 198 | case 2: 199 | if (arg.length == 2) 200 | break; // ill-formed flag, we don't need to care here 201 | const st = arg[2] == '=' ? 3 : 2; 202 | const path = getPath(arg[st .. $]); 203 | if (mark == 1) 204 | imports.put(path); 205 | else 206 | stringImports.put(path); 207 | break; 208 | case 3: 209 | fileImports.put(getPath(arg[fileImportMark.length .. $])); 210 | break; 211 | case 4: 212 | versions.put(getPath(arg[versionMark.length .. $])); 213 | break; 214 | case 5: 215 | debugVersions.put(getPath(arg[debugMark.length .. $])); 216 | break; 217 | default: 218 | break; 219 | } 220 | } 221 | } 222 | 223 | string getPath(string filename) 224 | { 225 | import std.path : absolutePath; 226 | 227 | return absolutePath(filename, directory); 228 | } 229 | } 230 | 231 | private string[] unescapeCommand(string cmd) 232 | { 233 | string[] result; 234 | string current; 235 | 236 | bool inquot; 237 | bool escapeNext; 238 | 239 | foreach (dchar c; cmd) 240 | { 241 | if (escapeNext) 242 | { 243 | escapeNext = false; 244 | if (c != '"') 245 | { 246 | current ~= '\\'; 247 | } 248 | current ~= c; 249 | continue; 250 | } 251 | 252 | switch (c) 253 | { 254 | case '\\': 255 | escapeNext = true; 256 | break; 257 | case '"': 258 | inquot = !inquot; 259 | break; 260 | case ' ': 261 | if (inquot) 262 | { 263 | current ~= ' '; 264 | } 265 | else 266 | { 267 | result ~= current; 268 | current = null; 269 | } 270 | break; 271 | default: 272 | current ~= c; 273 | break; 274 | } 275 | } 276 | 277 | if (current.length) 278 | { 279 | result ~= current; 280 | } 281 | return result; 282 | } 283 | 284 | @("unescapeCommand") 285 | unittest 286 | { 287 | const cmd = `"ldc2" "-I=..\foo\src" -I="..\with \" and space" "-m64" ` ~ 288 | `-of=foo/libfoo.a.p/src_foo_bar.d.obj -c ../foo/src/foo/bar.d`; 289 | 290 | const cmdArgs = unescapeCommand(cmd); 291 | 292 | const args = [ 293 | "ldc2", "-I=..\\foo\\src", "-I=..\\with \" and space", "-m64", 294 | "-of=foo/libfoo.a.p/src_foo_bar.d.obj", "-c", "../foo/src/foo/bar.d", 295 | ]; 296 | 297 | assert(cmdArgs == args); 298 | } 299 | -------------------------------------------------------------------------------- /dml/source/lookupgen.d: -------------------------------------------------------------------------------- 1 | import std.conv; 2 | import std.algorithm; 3 | import pegged.grammar; 4 | 5 | static import pegged.peg; 6 | 7 | /// 8 | enum CompletionType : ubyte 9 | { 10 | /// 11 | Undefined = 0, 12 | /// 13 | Class = 1, 14 | /// 15 | String = 2, 16 | /// 17 | Number = 3, 18 | /// 19 | Color = 4, 20 | /// 21 | EnumDefinition = 5, 22 | /// 23 | EnumValue = 6, 24 | /// 25 | Rectangle = 7, 26 | /// 27 | Boolean = 8, 28 | } 29 | 30 | struct CompletionLookup 31 | { 32 | CompletionItem item; 33 | string[][] providedScope = []; 34 | string[] requiredScope = []; 35 | } 36 | 37 | /// quote string: append double quotes, screen all special chars; 38 | /// so quoted string forms valid D string literal. 39 | /// allocates. 40 | string quote(const(char)[] s) 41 | { 42 | import std.array : appender; 43 | import std.format : formatElement, FormatSpec; 44 | 45 | auto res = appender!string(); 46 | FormatSpec!char fspc; // defaults to 's' 47 | formatElement(res, s, fspc); 48 | return res.data; 49 | } 50 | 51 | struct CompletionItem 52 | { 53 | CompletionType type; 54 | string value; 55 | string documentation = ""; 56 | string enumName = ""; 57 | 58 | string toString() 59 | { 60 | return "CompletionItem(CompletionType." ~ type.to!string ~ ", " 61 | ~ value.quote ~ ", " ~ documentation.quote ~ ", " ~ enumName.quote ~ ")"; 62 | } 63 | } 64 | 65 | mixin(grammar(` 66 | DML: 67 | Content < ( EnumBlock / ClassBlock )* 68 | Documentation <- :"---" :Spacing? ~(!endOfLine .)* :endOfLine 69 | EnumBlock < "enum" :Spacing identifier Documentation? :'{' EnumMember* :'}' 70 | EnumDefinition < identifier 71 | ClassBlock < "class" :Spacing identifier Documentation? :'{' ClassMember* :'}' 72 | ClassMember < "include" :Spacing identifier :';' 73 | / identifier :Spacing identifier :';' Documentation? 74 | EnumMember < identifier :';' Documentation? 75 | Comment <: "//" (!endOfLine .)* endOfLine 76 | Spacing <~ (space / endOfLine / Comment)* 77 | `)); 78 | 79 | struct ClassDependency 80 | { 81 | CompletionLookup type; 82 | CompletionLookup[] members; 83 | string[] remainingDependencies; 84 | } 85 | 86 | CompletionLookup[] generateCompletions(string completionLookup) 87 | { 88 | if (__ctfe) 89 | { 90 | assert(0); 91 | } 92 | CompletionLookup[] enumCompletions; 93 | CompletionLookup[] classCompletions; 94 | ParseTree tree = DML(completionLookup); 95 | assert(tree.successful); 96 | assert(tree.children.length == 1); 97 | assert(tree.children[0].name == "DML.Content"); 98 | ParseTree content = tree.children[0]; 99 | ClassDependency[] dependTree; 100 | string[] classes; 101 | string[] enums; 102 | 103 | foreach (child; content.children) 104 | { 105 | if (child.name == "DML.EnumBlock") 106 | { 107 | enums ~= child.matches[1]; 108 | foreach (val; child.children) 109 | { 110 | /// TODO: Add enum type itself 111 | if (val.name == "DML.EnumMember") 112 | { 113 | string documentation = ""; 114 | if (val.children.length > 0 && val.children[0].name == "DML.Documentation") 115 | documentation = val.children[0].matches[0]; 116 | enumCompletions ~= CompletionLookup(CompletionItem(CompletionType.EnumDefinition, 117 | val.matches[0], documentation, child.matches[1]), [], []); 118 | } 119 | } 120 | } 121 | else if (child.name == "DML.ClassBlock") 122 | classes ~= child.matches[1]; 123 | else 124 | assert(0); 125 | } 126 | 127 | foreach (child; content.children) 128 | { 129 | if (child.name == "DML.ClassBlock") 130 | { 131 | CompletionLookup[] members; 132 | string[] dependencies; 133 | foreach (member; child.children) 134 | { 135 | if (member.name == "DML.ClassMember") 136 | { 137 | string documentation = ""; 138 | if (member.children.length > 0 && member.children[0].name == "DML.Documentation") 139 | documentation = member.children[0].matches[0]; 140 | string type = member.matches[0]; 141 | string name = member.matches[1]; 142 | if (type == "include") 143 | dependencies ~= name; 144 | else if (type == "bool") 145 | members ~= CompletionLookup(CompletionItem(CompletionType.Boolean, 146 | name, documentation), [], [child.matches[1]]); 147 | else if (type == "color") 148 | members ~= CompletionLookup(CompletionItem(CompletionType.Color, 149 | name, documentation), [], [child.matches[1]]); 150 | else if (type == "number") 151 | members ~= CompletionLookup(CompletionItem(CompletionType.Number, 152 | name, documentation), [], [child.matches[1]]); 153 | else if (type == "rect") 154 | members ~= CompletionLookup(CompletionItem(CompletionType.Rectangle, 155 | name, documentation), [], [child.matches[1]]); 156 | else if (type == "string") 157 | members ~= CompletionLookup(CompletionItem(CompletionType.String, 158 | name, documentation), [], [child.matches[1]]); 159 | else 160 | { 161 | assert(!classes.canFind(type), "Member values can not be of type class!"); 162 | assert(enums.canFind(type), "Undefined value type '" ~ type ~ "'"); 163 | members ~= CompletionLookup(CompletionItem(CompletionType.EnumValue, 164 | name, documentation, type), [], [child.matches[1]]); 165 | } 166 | } 167 | } 168 | string[][] scopes; 169 | foreach (dep; dependencies) 170 | { 171 | if (!classes.canFind(dep)) 172 | assert(0, "Can't find class to include: " ~ dep); 173 | scopes ~= [dep]; 174 | } 175 | scopes ~= [child.matches[1]]; 176 | string documentation = ""; 177 | if (child.children.length > 0 && child.children[0].name == "DML.Documentation") 178 | documentation = child.children[0].matches[0]; 179 | CompletionLookup type = CompletionLookup(CompletionItem(CompletionType.Class, 180 | child.matches[1], documentation), scopes, []); 181 | dependTree ~= ClassDependency(type, members, dependencies); 182 | } 183 | } 184 | 185 | int i = 0; 186 | while (true) 187 | { 188 | if (i++ > 50) 189 | throw new Exception("Circular include"); 190 | bool done = true; 191 | foreach (ref dep; dependTree) 192 | { 193 | DepLoop: foreach_reverse (n, remaining; dep.remainingDependencies) 194 | { 195 | done = false; 196 | foreach (loaded; dependTree) 197 | { 198 | if (loaded.type.item.value == remaining 199 | && loaded.remainingDependencies.length == 0) 200 | { 201 | dep.type.providedScope ~= loaded.type.providedScope; 202 | string[][] newProvidedScope; 203 | foreach (sc; dep.type.providedScope) 204 | if (!newProvidedScope.canFind(sc)) 205 | newProvidedScope ~= sc; 206 | dep.type.providedScope = newProvidedScope; 207 | dep.remainingDependencies = dep.remainingDependencies.remove(n); 208 | continue DepLoop; 209 | } 210 | } 211 | } 212 | } 213 | if (done) 214 | break; 215 | } 216 | 217 | foreach (dep; dependTree) 218 | classCompletions ~= dep.type ~ dep.members; 219 | 220 | return enumCompletions ~ classCompletions; 221 | } 222 | 223 | void test() 224 | { 225 | import dunit.toolkit; 226 | 227 | string testCode = q{ 228 | enum Test --- description 1 229 | { 230 | A; --- A=2 231 | B; --- B=3 232 | C; 233 | D; --- D=5 234 | } 235 | class Something --- description 2 236 | { 237 | Test test; --- will do something 238 | number foo; --- bar 239 | string bar; --- foo 240 | bool test; --- no comment 241 | rect abc; 242 | } 243 | class Else 244 | { 245 | number def; --- extended 246 | include Something; 247 | } 248 | }; 249 | auto completions = generateCompletions(testCode); 250 | 251 | //dfmt off 252 | assertEqual(completions, [ 253 | CompletionLookup(CompletionItem(CompletionType.EnumDefinition, "A", "A=2", "Test")), 254 | CompletionLookup(CompletionItem(CompletionType.EnumDefinition, "B", "B=3", "Test")), 255 | CompletionLookup(CompletionItem(CompletionType.EnumDefinition, "C", "", "Test")), 256 | CompletionLookup(CompletionItem(CompletionType.EnumDefinition, "D", "D=5", "Test")), 257 | CompletionLookup(CompletionItem(CompletionType.Class, "Something", "description 2"), [["Something"]], []), 258 | CompletionLookup(CompletionItem(CompletionType.EnumValue, "test", "will do something", "Test"), [], ["Something"]), 259 | CompletionLookup(CompletionItem(CompletionType.Number, "foo", "bar"), [], ["Something"]), 260 | CompletionLookup(CompletionItem(CompletionType.String, "bar", "foo"), [], ["Something"]), 261 | CompletionLookup(CompletionItem(CompletionType.Boolean, "test", "no comment"), [], ["Something"]), 262 | CompletionLookup(CompletionItem(CompletionType.Rectangle, "abc", ""), [], ["Something"]), 263 | CompletionLookup(CompletionItem(CompletionType.Class, "Else", ""), [["Something"], ["Else"]], []), 264 | CompletionLookup(CompletionItem(CompletionType.Number, "def", "extended"), [], ["Else"]), 265 | ]); 266 | //dfmt on 267 | } 268 | -------------------------------------------------------------------------------- /source/workspaced/com/dlangui.d: -------------------------------------------------------------------------------- 1 | module workspaced.com.dlangui; 2 | 3 | import core.thread; 4 | import std.algorithm; 5 | import std.json; 6 | import std.process; 7 | import std.string; 8 | import std.uni; 9 | 10 | import painlessjson; 11 | 12 | import workspaced.api; 13 | import workspaced.completion.dml; 14 | 15 | @component("dlangui") 16 | class DlanguiComponent : ComponentWrapper 17 | { 18 | mixin DefaultComponentWrapper; 19 | 20 | /// Queries for code completion at position `pos` in DML code 21 | /// Returns: `[{type: CompletionType, value: string, documentation: string, enumName: string}]` 22 | /// Where type is an integer 23 | Future!(CompletionItem[]) complete(scope const(char)[] code, int pos) 24 | { 25 | auto ret = new typeof(return); 26 | gthreads.create({ 27 | mixin(traceTask); 28 | try 29 | { 30 | LocationInfo info = getLocationInfo(code, pos); 31 | CompletionItem[] suggestions; 32 | string name = info.itemScope[$ - 1]; 33 | string[] stack; 34 | if (info.itemScope.length > 1) 35 | stack = info.itemScope[0 .. $ - 1]; 36 | string[][] curScope = stack.getProvidedScope(); 37 | if (info.type == LocationType.RootMember) 38 | { 39 | foreach (CompletionLookup item; dmlCompletions) 40 | { 41 | if (item.item.type == CompletionType.Class) 42 | { 43 | if (name.length == 0 || item.item.value.canFind(name)) 44 | { 45 | suggestions ~= item.item; 46 | } 47 | } 48 | } 49 | } 50 | else if (info.type == LocationType.Member) 51 | { 52 | foreach (CompletionLookup item; dmlCompletions) 53 | { 54 | if (item.item.type == CompletionType.Class) 55 | { 56 | if (name.length == 0 || item.item.value.canFind(name)) 57 | { 58 | suggestions ~= item.item; 59 | } 60 | } 61 | else if (item.item.type != CompletionType.EnumDefinition) 62 | { 63 | if (curScope.canFind(item.requiredScope)) 64 | { 65 | if (name.length == 0 || item.item.value.canFind(name)) 66 | { 67 | suggestions ~= item.item; 68 | } 69 | } 70 | } 71 | } 72 | } 73 | else if (info.type == LocationType.PropertyValue) 74 | { 75 | foreach (CompletionLookup item; dmlCompletions) 76 | { 77 | if (item.item.type == CompletionType.EnumValue) 78 | { 79 | if (curScope.canFind(item.requiredScope)) 80 | { 81 | if (item.item.value == name) 82 | { 83 | foreach (CompletionLookup enumdef; dmlCompletions) 84 | { 85 | if (enumdef.item.type == CompletionType.EnumDefinition) 86 | { 87 | if (enumdef.item.enumName == item.item.enumName) 88 | suggestions ~= enumdef.item; 89 | } 90 | } 91 | break; 92 | } 93 | } 94 | } 95 | else if (item.item.type == CompletionType.Boolean) 96 | { 97 | if (curScope.canFind(item.requiredScope)) 98 | { 99 | if (item.item.value == name) 100 | { 101 | suggestions ~= CompletionItem(CompletionType.Keyword, "true"); 102 | suggestions ~= CompletionItem(CompletionType.Keyword, "false"); 103 | break; 104 | } 105 | } 106 | } 107 | } 108 | } 109 | ret.finish(suggestions); 110 | } 111 | catch (Throwable e) 112 | { 113 | ret.error(e); 114 | } 115 | }); 116 | return ret; 117 | } 118 | } 119 | 120 | /// 121 | enum CompletionType : ubyte 122 | { 123 | /// 124 | Undefined = 0, 125 | /// 126 | Class = 1, 127 | /// 128 | String = 2, 129 | /// 130 | Number = 3, 131 | /// 132 | Color = 4, 133 | /// 134 | EnumDefinition = 5, 135 | /// 136 | EnumValue = 6, 137 | /// 138 | Rectangle = 7, 139 | /// 140 | Boolean = 8, 141 | /// 142 | Keyword = 9, 143 | } 144 | 145 | /// Returned by list-completion 146 | struct CompletionItem 147 | { 148 | /// 149 | CompletionType type; 150 | /// 151 | string value; 152 | /// 153 | string documentation = ""; 154 | /// 155 | string enumName = ""; 156 | } 157 | 158 | struct CompletionLookup 159 | { 160 | CompletionItem item; 161 | string[][] providedScope = []; 162 | string[] requiredScope = []; 163 | } 164 | 165 | private: 166 | 167 | string[][] getProvidedScope(string[] stack) 168 | { 169 | if (stack.length == 0) 170 | return []; 171 | string[][] providedScope; 172 | foreach (CompletionLookup item; dmlCompletions) 173 | { 174 | if (item.item.type == CompletionType.Class) 175 | { 176 | if (item.item.value == stack[$ - 1]) 177 | { 178 | providedScope ~= item.providedScope; 179 | break; 180 | } 181 | } 182 | } 183 | return providedScope; 184 | } 185 | 186 | enum LocationType : ubyte 187 | { 188 | RootMember, 189 | Member, 190 | PropertyValue, 191 | None 192 | } 193 | 194 | struct LocationInfo 195 | { 196 | LocationType type; 197 | string[] itemScope; 198 | string propertyName; 199 | } 200 | 201 | LocationInfo getLocationInfo(scope const(char)[] code, int pos) 202 | { 203 | LocationInfo current; 204 | current.type = LocationType.RootMember; 205 | current.itemScope = []; 206 | current.propertyName = ""; 207 | string member = ""; 208 | bool inString = false; 209 | bool escapeChar = false; 210 | foreach (i, c; code) 211 | { 212 | if (i == pos) 213 | break; 214 | if (inString) 215 | { 216 | if (escapeChar) 217 | escapeChar = false; 218 | else 219 | { 220 | if (c == '\\') 221 | { 222 | escapeChar = true; 223 | } 224 | else if (c == '"') 225 | { 226 | inString = false; 227 | current.type = LocationType.None; 228 | member = ""; 229 | escapeChar = false; 230 | } 231 | } 232 | continue; 233 | } 234 | else 235 | { 236 | if (c == '{') 237 | { 238 | current.itemScope ~= member; 239 | current.propertyName = ""; 240 | member = ""; 241 | current.type = LocationType.Member; 242 | } 243 | else if (c == '\n' || c == '\r' || c == ';') 244 | { 245 | current.propertyName = ""; 246 | member = ""; 247 | current.type = LocationType.Member; 248 | } 249 | else if (c == ':') 250 | { 251 | current.propertyName = member; 252 | member = ""; 253 | current.type = LocationType.PropertyValue; 254 | } 255 | else if (c == '"') 256 | { 257 | inString = true; 258 | } 259 | else if (c == '}') 260 | { 261 | if (current.itemScope.length > 0) 262 | current.itemScope.length--; 263 | current.type = LocationType.None; 264 | current.propertyName = ""; 265 | member = ""; 266 | } 267 | else if (c.isWhite) 268 | { 269 | if (current.type == LocationType.None) 270 | current.type = LocationType.Member; 271 | if (current.itemScope.length == 0) 272 | current.type = LocationType.RootMember; 273 | } 274 | else 275 | { 276 | if (current.type == LocationType.Member || current.type == LocationType.RootMember) 277 | member ~= c; 278 | } 279 | } 280 | } 281 | if (member.length) 282 | current.propertyName = member; 283 | current.itemScope ~= current.propertyName; 284 | return current; 285 | } 286 | 287 | unittest 288 | { 289 | auto info = getLocationInfo(" ", 0); 290 | assert(info.type == LocationType.RootMember); 291 | info = getLocationInfo(`TableLayout { mar }`, 17); 292 | assert(info.itemScope == ["TableLayout", "mar"]); 293 | assert(info.type == LocationType.Member); 294 | info = getLocationInfo(`TableLayout { margins: 20; paddin }`, 33); 295 | assert(info.itemScope == ["TableLayout", "paddin"]); 296 | assert(info.type == LocationType.Member); 297 | info = getLocationInfo( 298 | "TableLayout { margins: 20; padding : 10\n\t\tTextWidget { text: \"} foo } }", 70); 299 | assert(info.itemScope == ["TableLayout", "TextWidget", "text"]); 300 | assert(info.type == LocationType.PropertyValue); 301 | info = getLocationInfo(`TableLayout { margins: 2 }`, 24); 302 | assert(info.itemScope == ["TableLayout", "margins"]); 303 | assert(info.type == LocationType.PropertyValue); 304 | info = getLocationInfo( 305 | "TableLayout { margins: 20; padding : 10\n\t\tTextWidget { text: \"} foobar\" } } ", int.max); 306 | assert(info.itemScope == [""]); 307 | assert(info.type == LocationType.RootMember); 308 | info = getLocationInfo( 309 | "TableLayout { margins: 20; padding : 10\n\t\tTextWidget { text: \"} foobar\"; } }", 69); 310 | assert(info.itemScope == ["TableLayout", "TextWidget", "text"]); 311 | assert(info.type == LocationType.PropertyValue); 312 | info = getLocationInfo("TableLayout {\n\t", int.max); 313 | assert(info.itemScope == ["TableLayout", ""]); 314 | assert(info.type == LocationType.Member); 315 | info = getLocationInfo(`TableLayout { 316 | colCount: 2 317 | margins: 20; padding: 10 318 | backgroundColor: "#FFFFE0" 319 | TextWidget { 320 | t`, int.max); 321 | assert(info.itemScope == ["TableLayout", "TextWidget", "t"]); 322 | assert(info.type == LocationType.Member); 323 | } 324 | -------------------------------------------------------------------------------- /source/workspaced/com/dfmt.d: -------------------------------------------------------------------------------- 1 | module workspaced.com.dfmt; 2 | 3 | import fs = std.file; 4 | import std.algorithm; 5 | import std.array; 6 | import std.conv; 7 | import std.getopt; 8 | import std.json; 9 | import std.stdio : stderr; 10 | import std.string; 11 | 12 | import dfmt.config; 13 | import dfmt.editorconfig; 14 | import dfmt.formatter : fmt = format; 15 | 16 | import dparse.lexer; 17 | 18 | import core.thread; 19 | 20 | import painlessjson; 21 | 22 | import workspaced.api; 23 | 24 | @component("dfmt") 25 | class DfmtComponent : ComponentWrapper 26 | { 27 | mixin DefaultComponentWrapper; 28 | 29 | /// Will format the code passed in asynchronously. 30 | /// Returns: the formatted code as string 31 | Future!string format(scope const(char)[] code, string[] arguments = []) 32 | { 33 | mixin(gthreadsAsyncProxy!`formatSync(code, arguments)`); 34 | } 35 | 36 | /// Will format the code passed in synchronously. Might take a short moment on larger documents. 37 | /// Returns: the formatted code as string 38 | string formatSync(scope const(char)[] code, string[] arguments = []) 39 | { 40 | Config config; 41 | config.initializeWithDefaults(); 42 | string configPath; 43 | if (getConfigPath("dfmt.json", configPath)) 44 | { 45 | stderr.writeln("Overriding dfmt arguments with workspace-d dfmt.json config file"); 46 | try 47 | { 48 | auto json = parseJSON(fs.readText(configPath)); 49 | foreach (i, ref member; config.tupleof) 50 | { 51 | enum name = __traits(identifier, config.tupleof[i]); 52 | if (name.startsWith("dfmt_")) 53 | json.tryFetchProperty(member, name["dfmt_".length .. $]); 54 | else 55 | json.tryFetchProperty(member, name); 56 | } 57 | } 58 | catch (Exception e) 59 | { 60 | stderr.writeln("dfmt.json in workspace-d config folder is malformed"); 61 | stderr.writeln(e); 62 | } 63 | } 64 | else if (arguments.length) 65 | { 66 | // code for parsing args from dfmt main.d (keep up-to-date!) 67 | // https://github.com/dlang-community/dfmt/blob/master/src/dfmt/main.d 68 | void handleBooleans(string option, string value) 69 | { 70 | import dfmt.editorconfig : OptionalBoolean; 71 | import std.exception : enforce; 72 | 73 | enforce!GetOptException(value == "true" || value == "false", "Invalid argument"); 74 | immutable OptionalBoolean val = value == "true" ? OptionalBoolean.t : OptionalBoolean.f; 75 | switch (option) 76 | { 77 | case "align_switch_statements": 78 | config.dfmt_align_switch_statements = val; 79 | break; 80 | case "outdent_attributes": 81 | config.dfmt_outdent_attributes = val; 82 | break; 83 | case "space_after_cast": 84 | config.dfmt_space_after_cast = val; 85 | break; 86 | case "space_before_function_parameters": 87 | config.dfmt_space_before_function_parameters = val; 88 | break; 89 | case "split_operator_at_line_end": 90 | config.dfmt_split_operator_at_line_end = val; 91 | break; 92 | case "selective_import_space": 93 | config.dfmt_selective_import_space = val; 94 | break; 95 | case "compact_labeled_statements": 96 | config.dfmt_compact_labeled_statements = val; 97 | break; 98 | case "single_template_constraint_indent": 99 | config.dfmt_single_template_constraint_indent = val; 100 | break; 101 | case "space_before_aa_colon": 102 | config.dfmt_space_before_aa_colon = val; 103 | break; 104 | case "keep_line_breaks": 105 | config.dfmt_keep_line_breaks = val; 106 | break; 107 | case "single_indent": 108 | config.dfmt_single_indent = val; 109 | break; 110 | default: 111 | throw new Exception("Invalid command-line switch"); 112 | } 113 | } 114 | 115 | arguments = "dfmt" ~ arguments; 116 | 117 | // this too keep up-to-date 118 | // everything except "version", "config", "help", "inplace" arguments 119 | 120 | //dfmt off 121 | getopt(arguments, 122 | "align_switch_statements", &handleBooleans, 123 | "brace_style", &config.dfmt_brace_style, 124 | "end_of_line", &config.end_of_line, 125 | "indent_size", &config.indent_size, 126 | "indent_style|t", &config.indent_style, 127 | "max_line_length", &config.max_line_length, 128 | "soft_max_line_length", &config.dfmt_soft_max_line_length, 129 | "outdent_attributes", &handleBooleans, 130 | "space_after_cast", &handleBooleans, 131 | "selective_import_space", &handleBooleans, 132 | "space_before_function_parameters", &handleBooleans, 133 | "split_operator_at_line_end", &handleBooleans, 134 | "compact_labeled_statements", &handleBooleans, 135 | "single_template_constraint_indent", &handleBooleans, 136 | "space_before_aa_colon", &handleBooleans, 137 | "tab_width", &config.tab_width, 138 | "template_constraint_style", &config.dfmt_template_constraint_style, 139 | "keep_line_breaks", &handleBooleans, 140 | "single_indent", &handleBooleans, 141 | ); 142 | //dfmt on 143 | } 144 | auto output = appender!string; 145 | fmt("stdin", cast(ubyte[]) code, output, &config); 146 | if (output.data.length) 147 | return output.data; 148 | else 149 | return code.idup; 150 | } 151 | 152 | /// Finds dfmt instruction comments (dfmt off, dfmt on) 153 | /// Returns: a list of dfmt instructions, sorted in appearing (source code) 154 | /// order 155 | DfmtInstruction[] findDfmtInstructions(scope const(char)[] code) 156 | { 157 | LexerConfig config; 158 | config.whitespaceBehavior = WhitespaceBehavior.skip; 159 | config.commentBehavior = CommentBehavior.noIntern; 160 | auto lexer = DLexer(code, config, &workspaced.stringCache); 161 | auto ret = appender!(DfmtInstruction[]); 162 | Search: foreach (token; lexer) 163 | { 164 | if (token.type == tok!"comment") 165 | { 166 | auto text = dfmtCommentText(token.text); 167 | DfmtInstruction instruction; 168 | switch (text) 169 | { 170 | case "dfmt on": 171 | instruction.type = DfmtInstruction.Type.dfmtOn; 172 | break; 173 | case "dfmt off": 174 | instruction.type = DfmtInstruction.Type.dfmtOff; 175 | break; 176 | default: 177 | text = text.chompPrefix("/").strip; // make doc comments (///) appear as unknown because only first 2 // are stripped. 178 | if (text.startsWith("dfmt", "dmft", "dftm")) // include some typos 179 | { 180 | instruction.type = DfmtInstruction.Type.unknown; 181 | break; 182 | } 183 | continue Search; 184 | } 185 | instruction.index = token.index; 186 | instruction.line = token.line; 187 | instruction.column = token.column; 188 | instruction.length = token.text.length; 189 | ret.put(instruction); 190 | } 191 | else if (token.type == tok!"__EOF__") 192 | break; 193 | } 194 | return ret.data; 195 | } 196 | } 197 | 198 | /// 199 | struct DfmtInstruction 200 | { 201 | /// Known instruction types 202 | enum Type 203 | { 204 | /// Instruction to turn off formatting from here 205 | dfmtOff, 206 | /// Instruction to turn on formatting again from here 207 | dfmtOn, 208 | /// Starts with dfmt, but unknown contents 209 | unknown, 210 | } 211 | 212 | /// 213 | Type type; 214 | /// libdparse Token location (byte based offset) 215 | size_t index; 216 | /// libdparse Token location (byte based, 1-based) 217 | size_t line, column; 218 | /// Comment length in bytes 219 | size_t length; 220 | } 221 | 222 | private: 223 | 224 | // from dfmt/formatter.d TokenFormatter!T.commentText 225 | string dfmtCommentText(string commentText) 226 | { 227 | import std.string : strip; 228 | 229 | if (commentText[0 .. 2] == "//") 230 | commentText = commentText[2 .. $]; 231 | else 232 | { 233 | if (commentText.length > 3) 234 | commentText = commentText[2 .. $ - 2]; 235 | else 236 | commentText = commentText[2 .. $]; 237 | } 238 | return commentText.strip(); 239 | } 240 | 241 | void tryFetchProperty(T = string)(ref JSONValue json, ref T ret, string name) 242 | { 243 | auto ptr = name in json; 244 | if (ptr) 245 | { 246 | auto val = *ptr; 247 | static if (is(T == string) || is(T == enum)) 248 | { 249 | if (val.type != JSONType.string) 250 | throw new Exception("dfmt config value '" ~ name ~ "' must be a string"); 251 | static if (is(T == enum)) 252 | ret = val.str.to!T; 253 | else 254 | ret = val.str; 255 | } 256 | else static if (is(T == uint)) 257 | { 258 | if (val.type != JSONType.integer) 259 | throw new Exception("dfmt config value '" ~ name ~ "' must be a number"); 260 | if (val.integer < 0) 261 | throw new Exception("dfmt config value '" ~ name ~ "' must be a positive number"); 262 | ret = cast(T) val.integer; 263 | } 264 | else static if (is(T == int)) 265 | { 266 | if (val.type != JSONType.integer) 267 | throw new Exception("dfmt config value '" ~ name ~ "' must be a number"); 268 | ret = cast(T) val.integer; 269 | } 270 | else static if (is(T == OptionalBoolean)) 271 | { 272 | if (val.type != JSONType.true_ && val.type != JSONType.false_) 273 | throw new Exception("dfmt config value '" ~ name ~ "' must be a boolean"); 274 | ret = val.type == JSONType.true_ ? OptionalBoolean.t : OptionalBoolean.f; 275 | } 276 | else 277 | static assert(false); 278 | } 279 | } 280 | 281 | unittest 282 | { 283 | scope backend = new WorkspaceD(); 284 | auto workspace = makeTemporaryTestingWorkspace; 285 | auto instance = backend.addInstance(workspace.directory); 286 | backend.register!DfmtComponent; 287 | DfmtComponent dfmt = instance.get!DfmtComponent; 288 | 289 | assert(dfmt.findDfmtInstructions("void main() {}").length == 0); 290 | assert(dfmt.findDfmtInstructions("void main() {\n\t// dfmt off\n}") == [ 291 | DfmtInstruction(DfmtInstruction.Type.dfmtOff, 15, 2, 2, 11) 292 | ]); 293 | assert(dfmt.findDfmtInstructions(`import std.stdio; 294 | 295 | // dfmt on 296 | void main() 297 | { 298 | // dfmt off 299 | writeln("hello"); 300 | // dmft off 301 | string[string] x = [ 302 | "a": "b" 303 | ]; 304 | // dfmt on 305 | }`) == [ 306 | DfmtInstruction(DfmtInstruction.Type.dfmtOn, 19, 3, 1, 10), 307 | DfmtInstruction(DfmtInstruction.Type.dfmtOff, 45, 6, 2, 11), 308 | DfmtInstruction(DfmtInstruction.Type.unknown, 77, 8, 2, 11), 309 | DfmtInstruction(DfmtInstruction.Type.dfmtOn, 127, 12, 2, 10), 310 | ]); 311 | } 312 | -------------------------------------------------------------------------------- /source/workspaced/com/snippets/generator.d: -------------------------------------------------------------------------------- 1 | module workspaced.com.snippets.generator; 2 | 3 | // debug = TraceGenerator; 4 | 5 | import dparse.ast; 6 | import dparse.lexer; 7 | 8 | import workspaced.api; 9 | import workspaced.com.snippets; 10 | import workspaced.dparseext; 11 | 12 | import std.algorithm; 13 | import std.array; 14 | import std.conv; 15 | import std.meta : AliasSeq; 16 | 17 | /// Checks if a variable is suitable to be iterated over and finds out information about it 18 | /// Params: 19 | /// ret = loop scope to manipulate 20 | /// variable = variable to check 21 | /// Returns: true if this variable is suitable for iteration, false if not 22 | bool fillLoopScopeInfo(ref SnippetLoopScope ret, const scope VariableUsage variable) 23 | { 24 | // try to minimize false positives as much as possible here! 25 | // the first true result will stop the whole loop variable finding process 26 | 27 | if (!variable.name.text.length) 28 | return false; 29 | 30 | if (variable.type) 31 | { 32 | if (variable.type.typeSuffixes.length) 33 | { 34 | if (variable.type.typeSuffixes[$ - 1].array) 35 | { 36 | const isMap = !!variable.type.typeSuffixes[$ - 1].type; 37 | 38 | ret.stringIterator = variable.type.type2 39 | && variable.type.type2.builtinType.among!(tok!"char", tok!"wchar", tok!"dchar"); 40 | ret.type = formatType(variable.type.typeConstructors, 41 | variable.type.typeSuffixes[0 .. $ - 1], variable.type.type2); 42 | ret.iterator = variable.name.text; 43 | ret.numItems = isMap ? 2 : 1; 44 | return true; 45 | } 46 | } 47 | else if (variable.type.type2 && variable.type.type2.typeIdentifierPart) 48 | { 49 | // hardcode string, wstring and dstring 50 | const t = variable.type.type2.typeIdentifierPart; 51 | if (!t.dot && !t.indexer && !t.typeIdentifierPart && t.identifierOrTemplateInstance) 52 | { 53 | const simpleTypeName = t.identifierOrTemplateInstance.identifier.tokenText; 54 | switch (simpleTypeName) 55 | { 56 | case "string": 57 | case "wstring": 58 | case "dstring": 59 | ret.stringIterator = true; 60 | ret.iterator = variable.name.text; 61 | return true; 62 | default: 63 | break; 64 | } 65 | } 66 | } 67 | } 68 | 69 | if (variable.value) 70 | { 71 | if (variable.value.arrayInitializer) 72 | { 73 | bool isMap; 74 | auto items = variable.value.arrayInitializer.arrayMemberInitializations; 75 | if (items.length) 76 | isMap = !!items[0].assignExpression; // this is the value before the ':' or null for no key 77 | 78 | ret.stringIterator = false; 79 | ret.iterator = variable.name.text; 80 | ret.numItems = isMap ? 2 : 1; 81 | return true; 82 | } 83 | else if (variable.value.assignExpression) 84 | { 85 | // TODO: determine if this is a loop variable based on value 86 | } 87 | } 88 | 89 | return false; 90 | } 91 | 92 | string formatType(const IdType[] typeConstructors, const TypeSuffix[] typeSuffixes, const Type2 type2) @trusted 93 | { 94 | Type t = new Type(); 95 | t.typeConstructors = cast(IdType[]) typeConstructors; 96 | t.typeSuffixes = cast(TypeSuffix[]) typeSuffixes; 97 | t.type2 = cast(Type2) type2; 98 | return astToString(t); 99 | } 100 | 101 | /// Helper struct containing variable definitions and notable assignments which 102 | struct VariableUsage 103 | { 104 | const Type type; 105 | const Token name; 106 | const NonVoidInitializer value; 107 | } 108 | 109 | unittest 110 | { 111 | import std.experimental.logger : globalLogLevel, LogLevel; 112 | 113 | globalLogLevel = LogLevel.trace; 114 | 115 | scope backend = new WorkspaceD(); 116 | auto workspace = makeTemporaryTestingWorkspace; 117 | auto instance = backend.addInstance(workspace.directory); 118 | backend.register!SnippetsComponent; 119 | SnippetsComponent snippets = backend.get!SnippetsComponent(workspace.directory); 120 | 121 | static immutable loopCode = `module something; 122 | 123 | void foo() 124 | { 125 | int[] x = [1, 2, 3]; 126 | // trivial loop 127 | 128 | } 129 | 130 | void bar() 131 | { 132 | auto houseNumbers = [1, 2, 3]; 133 | // non-trivial (auto) loop 134 | 135 | } 136 | 137 | void existing() 138 | { 139 | int[3] items = [1, 2, 3]; 140 | int item; 141 | // clashing name 142 | 143 | } 144 | 145 | void strings() 146 | { 147 | string x; 148 | int y; 149 | // characters of x 150 | 151 | } 152 | 153 | void noValue() 154 | { 155 | int hello; 156 | // no lists 157 | 158 | } 159 | 160 | void map() 161 | { 162 | auto map = ["hello": "world", "key": "value"]; 163 | // key, value 164 | 165 | }`; 166 | 167 | SnippetInfo i; 168 | SnippetLoopScope s; 169 | 170 | i = snippets.determineSnippetInfo(null, loopCode, 18); 171 | assert(i.level == SnippetLevel.global); 172 | s = i.loopScope; // empty 173 | assert(s == SnippetLoopScope.init); 174 | 175 | i = snippets.determineSnippetInfo(null, loopCode, 30); 176 | assert(i.level == SnippetLevel.other, i.stack.to!string); 177 | s = i.loopScope; // empty 178 | assert(s == SnippetLoopScope.init); 179 | 180 | i = snippets.determineSnippetInfo(null, loopCode, 31); 181 | assert(i.level == SnippetLevel.method, i.stack.to!string); 182 | i = snippets.determineSnippetInfo(null, loopCode, 73); 183 | assert(i.level == SnippetLevel.method, i.stack.to!string); 184 | i = snippets.determineSnippetInfo(null, loopCode, 74); 185 | assert(i.level == SnippetLevel.global, i.stack.to!string); 186 | 187 | i = snippets.determineSnippetInfo(null, loopCode, 43); 188 | assert(i.level == SnippetLevel.value); 189 | s = i.loopScope; // in value 190 | assert(s == SnippetLoopScope.init); 191 | 192 | i = snippets.determineSnippetInfo(null, loopCode, 72); 193 | assert(i.level == SnippetLevel.method); 194 | s = i.loopScope; // trivial of x 195 | assert(s.supported); 196 | assert(!s.stringIterator); 197 | assert(s.type == "int"); 198 | assert(s.iterator == "x"); 199 | 200 | i = snippets.determineSnippetInfo(null, loopCode, 150); 201 | assert(i.level == SnippetLevel.method); 202 | s = i.loopScope; // non-trivial of houseNumbers (should be named houseNumber) 203 | assert(s.supported); 204 | assert(!s.stringIterator); 205 | assert(null == s.type); 206 | assert(s.iterator == "houseNumbers"); 207 | 208 | i = snippets.determineSnippetInfo(null, loopCode, 229); 209 | assert(i.level == SnippetLevel.method); 210 | s = i.loopScope; // non-trivial of items with existing variable name 211 | assert(s.supported); 212 | assert(!s.stringIterator); 213 | assert(s.type == "int"); 214 | assert(s.iterator == "items"); 215 | 216 | i = snippets.determineSnippetInfo(null, loopCode, 290); 217 | assert(i.level == SnippetLevel.method); 218 | s = i.loopScope; // string iteration 219 | assert(s.supported); 220 | assert(s.stringIterator); 221 | assert(null == s.type); 222 | assert(s.iterator == "x"); 223 | 224 | i = snippets.determineSnippetInfo(null, loopCode, 337); 225 | assert(i.level == SnippetLevel.method); 226 | s = i.loopScope; // no predefined variable 227 | assert(s.supported); 228 | assert(!s.stringIterator); 229 | assert(null == s.type); 230 | assert(null == s.iterator); 231 | 232 | i = snippets.determineSnippetInfo(null, loopCode, 418); 233 | assert(i.level == SnippetLevel.method); 234 | s = i.loopScope; // hash map 235 | assert(s.supported); 236 | assert(!s.stringIterator); 237 | assert(null == s.type); 238 | assert(s.iterator == "map"); 239 | assert(s.numItems == 2); 240 | } 241 | 242 | enum StackStorageScope(string val) = "if (done) return; auto __" ~ val 243 | ~ "_scope = " ~ val ~ "; scope (exit) if (!done) " ~ val ~ " = __" ~ val ~ "_scope;"; 244 | enum SnippetLevelWrapper(SnippetLevel level) = "if (done) return; pushLevel(" 245 | ~ level.stringof ~ ", dec); scope (exit) popLevel(dec); " 246 | ~ "if (!dec.tokens.length || dec.tokens[0].index <= position) lastStatement = null;"; 247 | enum FullSnippetLevelWrapper(SnippetLevel level) = SnippetLevelWrapper!level ~ " super.visit(dec);"; 248 | 249 | class SnippetInfoGenerator : ASTVisitor 250 | { 251 | alias visit = ASTVisitor.visit; 252 | 253 | this(size_t position) 254 | { 255 | this.position = position; 256 | } 257 | 258 | static foreach (T; AliasSeq!(Declaration, ImportBindings, ImportBind, ModuleDeclaration)) 259 | override void visit(const T dec) 260 | { 261 | mixin(FullSnippetLevelWrapper!(SnippetLevel.other)); 262 | } 263 | 264 | override void visit(const MixinTemplateDeclaration dec) 265 | { 266 | mixin(SnippetLevelWrapper!(SnippetLevel.mixinTemplate)); 267 | // avoid TemplateDeclaration overriding scope, immediately iterate over children 268 | if (dec.templateDeclaration) 269 | dec.templateDeclaration.accept(this); 270 | } 271 | 272 | override void visit(const StructBody dec) 273 | { 274 | mixin(FullSnippetLevelWrapper!(SnippetLevel.type)); 275 | } 276 | 277 | static foreach (T; AliasSeq!(SpecifiedFunctionBody, Unittest)) 278 | override void visit(const T dec) 279 | { 280 | mixin(StackStorageScope!"variableStack"); 281 | mixin(FullSnippetLevelWrapper!(SnippetLevel.method)); 282 | } 283 | 284 | override void visit(const StatementNoCaseNoDefault dec) 285 | { 286 | mixin(StackStorageScope!"variableStack"); 287 | super.visit(dec); 288 | } 289 | 290 | override void visit(const DeclarationOrStatement dec) 291 | { 292 | super.visit(dec); 293 | if (!dec.tokens.length || dec.tokens[0].index <= position) 294 | lastStatement = cast()dec; 295 | } 296 | 297 | static foreach (T; AliasSeq!(Arguments, ExpressionNode)) 298 | override void visit(const T dec) 299 | { 300 | mixin(FullSnippetLevelWrapper!(SnippetLevel.value)); 301 | } 302 | 303 | override void visit(const VariableDeclaration dec) 304 | { 305 | // not quite accurate for VariableDeclaration, should only be value after = sign 306 | mixin(SnippetLevelWrapper!(SnippetLevel.value)); 307 | super.visit(dec); 308 | 309 | foreach (t; dec.declarators) 310 | { 311 | debug(TraceGenerator) trace("push variable ", variableStack.length, " ", t.name.text, " of type ", 312 | astToString(dec.type), " and value ", astToString(t.initializer)); 313 | variableStack.assumeSafeAppend ~= VariableUsage(dec.type, t.name, 314 | t.initializer ? t.initializer.nonVoidInitializer : null); 315 | } 316 | 317 | if (dec.autoDeclaration) 318 | foreach (t; dec.autoDeclaration.parts) 319 | { 320 | debug(TraceGenerator) trace("push variable ", variableStack.length, " ", t.identifier.text, 321 | " of type auto and value ", astToString(t.initializer)); 322 | variableStack.assumeSafeAppend ~= VariableUsage(dec.type, t.identifier, 323 | t.initializer ? t.initializer.nonVoidInitializer : null); 324 | } 325 | } 326 | 327 | ref inout(SnippetInfo) value() inout 328 | { 329 | return ret; 330 | } 331 | 332 | void pushLevel(SnippetLevel level, const BaseNode node) 333 | { 334 | if (done) 335 | return; 336 | debug(TraceGenerator) trace("push ", level, " on ", typeid(node).name, " ", current, " -> ", node.tokens[0].index); 337 | 338 | if (node.tokens.length) 339 | { 340 | current = node.tokens[0].index; 341 | if (current >= position) 342 | { 343 | done = true; 344 | debug(TraceGenerator) trace("done"); 345 | return; 346 | } 347 | } 348 | ret.stack.assumeSafeAppend ~= level; 349 | } 350 | 351 | void popLevel(const BaseNode node) 352 | { 353 | if (done) 354 | return; 355 | debug(TraceGenerator) trace("pop from ", typeid(node).name, " ", current, " -> ", 356 | node.tokens[$ - 1].index + node.tokens[$ - 1].tokenText.length); 357 | 358 | if (node.tokens.length) 359 | { 360 | current = node.tokens[$ - 1].index + node.tokens[$ - 1].tokenText.length; 361 | if (current > position) 362 | { 363 | done = true; 364 | debug(TraceGenerator) trace("done"); 365 | return; 366 | } 367 | } 368 | 369 | ret.stack.length--; 370 | } 371 | 372 | bool done; 373 | VariableUsage[] variableStack; 374 | DeclarationOrStatement lastStatement; 375 | size_t position, current; 376 | SnippetInfo ret; 377 | } 378 | -------------------------------------------------------------------------------- /source/workspaced/dparseext.d: -------------------------------------------------------------------------------- 1 | module workspaced.dparseext; 2 | 3 | import std.algorithm; 4 | import std.array; 5 | import std.experimental.logger; 6 | import std.string; 7 | 8 | import dparse.ast; 9 | import dparse.lexer; 10 | import dparse.parser; 11 | import dparse.rollback_allocator; 12 | import dsymbol.builtin.names; 13 | import dsymbol.modulecache : ASTAllocator, ModuleCache; 14 | 15 | string makeString(in IdentifierOrTemplateChain c) 16 | { 17 | return c.identifiersOrTemplateInstances.map!(a => a.identifier.text).join("."); 18 | } 19 | 20 | string astToString(T, Args...)(in T ast, Args args) 21 | { 22 | import dparse.formatter : Formatter; 23 | 24 | if (!ast) 25 | return null; 26 | 27 | auto app = appender!string(); 28 | auto formatter = new Formatter!(typeof(app))(app); 29 | formatter.format(ast, args); 30 | return app.data; 31 | } 32 | 33 | string paramsToString(Dec)(const Dec dec) 34 | { 35 | import dparse.formatter : Formatter; 36 | 37 | auto app = appender!string(); 38 | auto formatter = new Formatter!(typeof(app))(app); 39 | 40 | static if (is(Dec == FunctionDeclaration) || is(Dec == Constructor)) 41 | { 42 | formatter.format(dec.parameters); 43 | } 44 | else static if (is(Dec == TemplateDeclaration)) 45 | { 46 | formatter.format(dec.templateParameters); 47 | } 48 | 49 | return app.data; 50 | } 51 | 52 | /// Other tokens 53 | private enum dynamicTokens = [ 54 | "specialTokenSequence", "comment", "identifier", "scriptLine", 55 | "whitespace", "doubleLiteral", "floatLiteral", "idoubleLiteral", 56 | "ifloatLiteral", "intLiteral", "longLiteral", "realLiteral", 57 | "irealLiteral", "uintLiteral", "ulongLiteral", "characterLiteral", 58 | "dstringLiteral", "stringLiteral", "wstringLiteral" 59 | ]; 60 | 61 | string tokenText(const Token token) 62 | { 63 | switch (token.type) 64 | { 65 | static foreach (T; dynamicTokens) 66 | { 67 | case tok!T: 68 | } 69 | return token.text; 70 | default: 71 | return str(token.type); 72 | } 73 | } 74 | 75 | size_t textLength(const Token token) 76 | { 77 | return token.tokenText.length; 78 | } 79 | 80 | bool isSomeString(const Token token) 81 | { 82 | switch (token.type) 83 | { 84 | case tok!"characterLiteral": 85 | case tok!"dstringLiteral": 86 | case tok!"stringLiteral": 87 | case tok!"wstringLiteral": 88 | return true; 89 | default: 90 | return false; 91 | } 92 | } 93 | 94 | bool isLikeIdentifier(const Token token) 95 | { 96 | import workspaced.helpers; 97 | 98 | auto text = token.tokenText; 99 | return text.length && text[0].isIdentifierChar; 100 | } 101 | 102 | /// Performs a binary search to find the token containing the search location. 103 | /// Params: 104 | /// tokens = the token array to search in. 105 | /// bytes = the byte index the token should be in. 106 | /// Returns: the index of the token inside the given tokens array which 107 | /// contains the character specified at the given byte. This will be the first 108 | /// token that is `tok.index == bytes` or before the next token that is too far. 109 | /// If no tokens match, this will return `tokens.length`. 110 | /// 111 | /// This is equivalent to the following code: 112 | /// --- 113 | /// foreach (i, tok; tokens) 114 | /// { 115 | /// if (tok.index == bytes) 116 | /// return i; 117 | /// else if (tok.index > bytes) 118 | /// return i - 1; 119 | /// } 120 | /// return tokens.length; 121 | /// --- 122 | size_t tokenIndexAtByteIndex(scope const(Token)[] tokens, size_t bytes) 123 | out (v; v <= tokens.length) 124 | { 125 | if (!tokens.length || tokens[0].index >= bytes) 126 | return 0; 127 | 128 | // find where to start using binary search 129 | size_t l = 0; 130 | size_t r = tokens.length - 1; 131 | while (l < r) 132 | { 133 | size_t m = (l + r) / 2; 134 | if (tokens[m].index < bytes) 135 | l = m + 1; 136 | else 137 | r = m - 1; 138 | } 139 | size_t start = r; 140 | 141 | // search remaining with linear search 142 | foreach (i, tok; tokens[start .. $]) 143 | { 144 | if (tok.index == bytes) 145 | return start + i; 146 | else if (tok.index > bytes) 147 | return start + i - 1; 148 | } 149 | return tokens.length; 150 | } 151 | 152 | /// ditto 153 | size_t tokenIndexAtPosition(scope const(Token)[] tokens, uint line, uint column) 154 | out (v; v <= tokens.length) 155 | { 156 | int cmp(Token token) 157 | { 158 | if (token.line != line) 159 | return token.line < line ? -1 : 1; 160 | else if (token.column != column) 161 | return token.column < column ? -1 : 1; 162 | else 163 | return 0; 164 | } 165 | 166 | if (!tokens.length || cmp(tokens[0]) >= 0) 167 | return 0; 168 | 169 | // find where to start using binary search 170 | size_t l = 0; 171 | size_t r = tokens.length - 1; 172 | while (l < r) 173 | { 174 | size_t m = (l + r) / 2; 175 | if (cmp(tokens[m]) < 0) 176 | l = m + 1; 177 | else 178 | r = m - 1; 179 | } 180 | size_t start = r; 181 | 182 | // search remaining with linear search 183 | foreach (i, tok; tokens[start .. $]) 184 | { 185 | if (cmp(tok) == 0) 186 | return start + i; 187 | else if (cmp(tok) > 0) 188 | return start + i - 1; 189 | } 190 | return tokens.length; 191 | } 192 | 193 | /// 194 | unittest 195 | { 196 | StringCache stringCache = StringCache(StringCache.defaultBucketCount); 197 | const(Token)[] tokens = getTokensForParser(cast(ubyte[]) `module foo.bar; 198 | 199 | // ok 200 | void main(string[] args) 201 | { 202 | } 203 | 204 | /// documentation 205 | void foo() 206 | { 207 | } 208 | `, LexerConfig.init, &stringCache); 209 | 210 | auto get(size_t bytes) 211 | { 212 | auto i = tokens.tokenIndexAtByteIndex(bytes); 213 | if (i == tokens.length) 214 | return tok!"__EOF__"; 215 | return tokens[i].type; 216 | } 217 | 218 | assert(get(0) == tok!"module"); 219 | assert(get(4) == tok!"module"); 220 | assert(get(6) == tok!"module"); 221 | assert(get(7) == tok!"identifier"); 222 | assert(get(9) == tok!"identifier"); 223 | assert(get(10) == tok!"."); 224 | assert(get(11) == tok!"identifier"); 225 | assert(get(16) == tok!";"); 226 | assert(get(49) == tok!"{"); 227 | assert(get(48) == tok!"{"); 228 | assert(get(47) == tok!")"); 229 | assert(get(1000) == tok!"__EOF__"); 230 | 231 | // TODO: process trivia fields in libdparse >=0.15.0 when it releases 232 | //assert(get(20) == tok!"comment"); 233 | assert(get(20) == tok!";"); 234 | 235 | // assert(get(57) == tok!"comment"); 236 | } 237 | 238 | bool isSomeString(const IdType type) 239 | { 240 | switch (type) 241 | { 242 | case tok!"stringLiteral": 243 | case tok!"wstringLiteral": 244 | case tok!"dstringLiteral": 245 | return true; 246 | default: 247 | return false; 248 | } 249 | } 250 | 251 | /// Tries to evaluate an expression if it evaluates to a string. 252 | /// Returns: `null` if the resulting value is not a string or could not be 253 | /// evaluated. 254 | string evaluateExpressionString(const PrimaryExpression expr) 255 | in (expr !is null) 256 | { 257 | return evaluateExpressionString(expr.primary); 258 | } 259 | 260 | /// ditto 261 | string evaluateExpressionString(const UnaryExpression expr) 262 | in (expr !is null) 263 | { 264 | if (expr.primaryExpression) 265 | return evaluateExpressionString(expr.primaryExpression); 266 | else 267 | return null; 268 | } 269 | 270 | /// ditto 271 | string evaluateExpressionString(const ExpressionNode expr) 272 | in (expr !is null) 273 | { 274 | // maybe we want to support simple concatenation here some time 275 | 276 | if (auto unary = cast(UnaryExpression) expr) 277 | return evaluateExpressionString(unary); 278 | else 279 | return null; 280 | } 281 | 282 | /// ditto 283 | string evaluateExpressionString(const Token token) 284 | { 285 | import dparse.strings : unescapeString, isStringLiteral; 286 | 287 | switch (token.type) 288 | { 289 | case tok!"stringLiteral": 290 | case tok!"wstringLiteral": 291 | case tok!"dstringLiteral": 292 | auto str = token.text; 293 | 294 | // we want to unquote here 295 | // foreach because implicit concatenation can combine multiple strings 296 | auto ret = appender!string; 297 | scope StringCache cache = StringCache(16); 298 | LexerConfig config; 299 | config.commentBehavior = CommentBehavior.noIntern; 300 | config.stringBehavior = StringBehavior.source; 301 | config.whitespaceBehavior = WhitespaceBehavior.skip; 302 | config.fileName = "evaluate-string-stdin"; 303 | foreach (t; DLexer(str, config, &cache)) 304 | { 305 | switch (t.type) 306 | { 307 | case tok!"stringLiteral": 308 | case tok!"wstringLiteral": 309 | case tok!"dstringLiteral": 310 | if (t.text.isStringLiteral) 311 | { 312 | ret ~= unescapeString(t.text); 313 | } 314 | else 315 | { 316 | debug 317 | { 318 | throw new Exception("Invalid stringLiteral in stringLiteral token: `" ~ t.text ~ '`'); 319 | } 320 | else 321 | { 322 | warningf("Invalid stringLiteral in stringLiteral token: `%s`", t.text); 323 | return str; 324 | } 325 | } 326 | break; 327 | default: 328 | // unexpected token, return input because it might already be 329 | // unescaped 330 | return str; 331 | } 332 | } 333 | 334 | return ret.data; 335 | default: 336 | return null; 337 | } 338 | } 339 | 340 | /// Finds the deepest non-null node of any BaseNode. (like visiting the tree) 341 | /// Aborts on types that contain `DeclarationOrStatement` or `Declaration[]` 342 | /// fields. 343 | /// Useful for getting the IfStatement out of a DeclarationOrStatement without 344 | /// traversing its children. 345 | BaseNode findDeepestNonBlockNode(T : BaseNode)(T ast) 346 | { 347 | static assert(!is(T == BaseNode), "Passed in a BaseNode, that's probably not what you wanted to do (pass in the most specific type you have)"); 348 | bool nonProcess = false; 349 | foreach (member; ast.tupleof) 350 | { 351 | static if (is(typeof(member) : DeclarationOrStatement) 352 | || is(typeof(member) : Declaration[])) 353 | { 354 | nonProcess = true; 355 | } 356 | } 357 | 358 | if (nonProcess) 359 | return ast; 360 | 361 | foreach (member; ast.tupleof) 362 | { 363 | static if (is(typeof(member) : BaseNode)) 364 | { 365 | if (member !is null) 366 | { 367 | return findDeepestNonBlockNode(member); 368 | } 369 | } 370 | } 371 | return ast; 372 | } 373 | 374 | /// Gets the final `else` block of an if. Will return a node of type 375 | /// `IfStatement` if it's an `else if` block. Returns null if there is no single 376 | /// else statement. 377 | BaseNode getIfElse(IfStatement ifStmt) 378 | { 379 | if (!ifStmt.elseStatement) 380 | return null; 381 | 382 | while (true) 383 | { 384 | auto elseStmt = ifStmt.elseStatement; 385 | if (!elseStmt) 386 | return ifStmt; 387 | 388 | auto stmtInElse = elseStmt.findDeepestNonBlockNode; 389 | assert(stmtInElse !is elseStmt); 390 | 391 | if (cast(IfStatement)stmtInElse) 392 | ifStmt = cast(IfStatement)stmtInElse; 393 | else 394 | return stmtInElse; 395 | } 396 | } 397 | 398 | unittest 399 | { 400 | StringCache stringCache = StringCache(StringCache.defaultBucketCount); 401 | RollbackAllocator rba; 402 | IfStatement parseIfStmt(string code) 403 | { 404 | const(Token)[] tokens = getTokensForParser(cast(ubyte[])code, LexerConfig.init, &stringCache); 405 | auto parser = new Parser(); 406 | parser.tokens = tokens; 407 | parser.allocator = &rba; 408 | return parser.parseIfStatement(); 409 | } 410 | 411 | alias p = parseIfStmt; 412 | assert(getIfElse(p("if (x) {}")) is null); 413 | assert(getIfElse(p("if (x) {} else if (y) {}")) !is null); 414 | assert(cast(IfStatement)getIfElse(p("if (x) {} else if (y) {}")) !is null, typeid(getIfElse(p("if (x) {} else if (y) {}"))).name); 415 | assert(getIfElse(p("if (x) {} else if (y) {} else {}")) !is null); 416 | assert(cast(IfStatement)getIfElse(p("if (x) {} else if (y) {} else {}")) is null); 417 | } 418 | 419 | C[] substr(C)(C[] s, size_t[2] range) 420 | { 421 | return substr(s, range[0], range[1]); 422 | } 423 | 424 | C[] substr(C)(C[] s, size_t start, size_t end) 425 | { 426 | if (!s.length) 427 | return s; 428 | if (start < 0) 429 | start = 0; 430 | if (start >= s.length) 431 | start = s.length - 1; // @suppress(dscanner.suspicious.length_subtraction) 432 | if (end > s.length) 433 | end = s.length; 434 | if (end < start) 435 | return s[start .. start]; 436 | return s[start .. end]; 437 | } 438 | -------------------------------------------------------------------------------- /source/app.d: -------------------------------------------------------------------------------- 1 | module app; 2 | 3 | import core.exception; 4 | import core.sync.mutex; 5 | import core.time; 6 | 7 | import painlessjson; 8 | import standardpaths; 9 | 10 | import workspaced.api; 11 | import workspaced.coms; 12 | 13 | import std.algorithm; 14 | import std.bitmanip; 15 | import std.datetime.stopwatch : StopWatch; 16 | import std.exception; 17 | import std.functional; 18 | import std.process; 19 | import std.stdio : File, stderr; 20 | import std.traits; 21 | 22 | static import std.conv; 23 | import std.json; 24 | import std.meta; 25 | import std.stdio; 26 | import std.string; 27 | 28 | __gshared File stdin, stdout; 29 | shared static this() 30 | { 31 | stdin = std.stdio.stdin; 32 | stdout = std.stdio.stdout; 33 | version (Windows) 34 | std.stdio.stdin = File("NUL", "r"); 35 | else version (Posix) 36 | std.stdio.stdin = File("/dev/null", "r"); 37 | else 38 | stderr.writeln("warning: no /dev/null implementation on this OS"); 39 | std.stdio.stdout = stderr; 40 | } 41 | 42 | __gshared Mutex writeMutex, commandMutex; 43 | 44 | void sendResponse(int id, JSONValue message) 45 | { 46 | synchronized (writeMutex) 47 | { 48 | ubyte[] data = nativeToBigEndian(id) ~ (cast(ubyte[]) message.toString()); 49 | stdout.rawWrite(nativeToBigEndian(cast(int) data.length) ~ data); 50 | stdout.flush(); 51 | } 52 | } 53 | 54 | void sendException(int id, Throwable t) 55 | { 56 | JSONValue[string] message; 57 | message["error"] = JSONValue(true); 58 | message["msg"] = JSONValue(t.msg); 59 | message["exception"] = JSONValue(t.toString); 60 | sendResponse(id, JSONValue(message)); 61 | } 62 | 63 | void broadcast(WorkspaceD workspaced, WorkspaceD.Instance instance, JSONValue message) 64 | { 65 | sendResponse(0x7F000000, JSONValue([ 66 | "workspace": JSONValue(instance ? instance.cwd : null), 67 | "data": message 68 | ])); 69 | } 70 | 71 | void bindFail(WorkspaceD.Instance instance, ComponentFactory component, Exception error) 72 | { 73 | sendResponse(0x7F000000, JSONValue([ 74 | "workspace": JSONValue(instance ? instance.cwd : null), 75 | "data": JSONValue([ 76 | "component": JSONValue(component.info.name), 77 | "type": JSONValue("bindfail"), 78 | "msg": JSONValue(error.msg), 79 | "trace": JSONValue(error.toString) 80 | ]) 81 | ])); 82 | } 83 | 84 | WorkspaceD engine; 85 | 86 | void handleRequest(int id, JSONValue request) 87 | { 88 | if (request.type != JSONType.object || "cmd" !in request 89 | || request["cmd"].type != JSONType.string) 90 | { 91 | goto printUsage; 92 | } 93 | else if (request["cmd"].str == "version") 94 | { 95 | version (unittest) 96 | sendResponse(id, JSONValue(null)); 97 | else 98 | { 99 | import workspaced.info : getVersionInfoJson; 100 | 101 | sendResponse(id, getVersionInfoJson); 102 | } 103 | } 104 | else if (request["cmd"].str == "load") 105 | { 106 | if ("component" !in request || request["component"].type != JSONType.string) 107 | { 108 | sendException(id, 109 | new Exception( 110 | `Expected load message to be in format {"cmd":"load", "component":string, ("autoregister":bool)}`)); 111 | } 112 | else 113 | { 114 | bool autoRegister = true; 115 | if (auto v = "autoregister" in request) 116 | autoRegister = v.type != JSONType.false_; 117 | string[] allComponents; 118 | static foreach (Component; AllComponents) 119 | allComponents ~= getUDAs!(Component, ComponentInfoParams)[0].name; 120 | ComponentSwitch: 121 | switch (request["component"].str) 122 | { 123 | static foreach (Component; AllComponents) 124 | { 125 | case getUDAs!(Component, ComponentInfoParams)[0].name: 126 | engine.register!Component(autoRegister); 127 | break ComponentSwitch; 128 | } 129 | default: 130 | sendException(id, 131 | new Exception( 132 | "Unknown Component '" ~ request["component"].str ~ "', built-in are " ~ allComponents.join( 133 | ", "))); 134 | return; 135 | } 136 | sendResponse(id, JSONValue(true)); 137 | } 138 | } 139 | else if (request["cmd"].str == "new") 140 | { 141 | if ("cwd" !in request || request["cwd"].type != JSONType.string) 142 | { 143 | sendException(id, 144 | new Exception( 145 | `Expected new message to be in format {"cmd":"new", "cwd":string, ("config":object)}`)); 146 | } 147 | else 148 | { 149 | string cwd = request["cwd"].str; 150 | if ("config" in request) 151 | engine.addInstance(cwd, Configuration(request["config"])); 152 | else 153 | engine.addInstance(cwd); 154 | sendResponse(id, JSONValue(true)); 155 | } 156 | } 157 | else if (request["cmd"].str == "config-set") 158 | { 159 | if ("config" !in request || request["config"].type != JSONType.object) 160 | { 161 | configSetFail: 162 | sendException(id, 163 | new Exception( 164 | `Expected new message to be in format {"cmd":"config-set", ("cwd":string), "config":object}`)); 165 | } 166 | else 167 | { 168 | if ("cwd" in request) 169 | { 170 | if (request["cwd"].type != JSONType.string) 171 | goto configSetFail; 172 | else 173 | engine.getInstance(request["cwd"].str).config.base = request["config"]; 174 | } 175 | else 176 | engine.globalConfiguration.base = request["config"]; 177 | sendResponse(id, JSONValue(true)); 178 | } 179 | } 180 | else if (request["cmd"].str == "config-get") 181 | { 182 | if ("cwd" in request) 183 | { 184 | if (request["cwd"].type != JSONType.string) 185 | sendException(id, 186 | new Exception( 187 | `Expected new message to be in format {"cmd":"config-get", ("cwd":string)}`)); 188 | else 189 | sendResponse(id, engine.getInstance(request["cwd"].str).config.base); 190 | } 191 | else 192 | sendResponse(id, engine.globalConfiguration.base); 193 | } 194 | else if (request["cmd"].str == "call") 195 | { 196 | JSONValue[] params; 197 | if ("params" in request) 198 | { 199 | if (request["params"].type != JSONType.array) 200 | goto callFail; 201 | params = request["params"].array; 202 | } 203 | if ("method" !in request || request["method"].type != JSONType.string 204 | || "component" !in request || request["component"].type != JSONType.string) 205 | { 206 | callFail: 207 | sendException(id, new Exception(`Expected call message to be in format {"cmd":"call", "component":string, "method":string, ("cwd":string), ("params":object[])}`)); 208 | } 209 | else 210 | { 211 | Future!JSONValue ret; 212 | string component = request["component"].str; 213 | string method = request["method"].str; 214 | if ("cwd" in request) 215 | { 216 | if (request["cwd"].type != JSONType.string) 217 | { 218 | goto callFail; 219 | } 220 | else 221 | { 222 | string cwd = request["cwd"].str; 223 | ret = engine.run(cwd, component, method, params); 224 | } 225 | } 226 | else 227 | ret = engine.run(component, method, params); 228 | 229 | ret.onDone = { 230 | if (ret.exception) 231 | sendException(id, ret.exception); 232 | else 233 | sendResponse(id, ret.value); 234 | }; 235 | } 236 | } 237 | else if (request["cmd"].str == "import-paths") 238 | { 239 | if ("cwd" !in request || request["cwd"].type != JSONType.string) 240 | sendException(id, 241 | new Exception(`Expected new message to be in format {"cmd":"import-paths", "cwd":string}`)); 242 | else 243 | sendResponse(id, engine.getInstance(request["cwd"].str).importPaths.toJSON); 244 | } 245 | else if (request["cmd"].str == "import-files") 246 | { 247 | if ("cwd" !in request || request["cwd"].type != JSONType.string) 248 | sendException(id, 249 | new Exception(`Expected new message to be in format {"cmd":"import-files", "cwd":string}`)); 250 | else 251 | sendResponse(id, engine.getInstance(request["cwd"].str).importFiles.toJSON); 252 | } 253 | else if (request["cmd"].str == "string-import-paths") 254 | { 255 | if ("cwd" !in request || request["cwd"].type != JSONType.string) 256 | sendException(id, 257 | new Exception( 258 | `Expected new message to be in format {"cmd":"string-import-paths", "cwd":string}`)); 259 | else 260 | sendResponse(id, engine.getInstance(request["cwd"].str).stringImportPaths.toJSON); 261 | } 262 | else 263 | { 264 | printUsage: 265 | sendException(id, new Exception("Invalid request, must contain a cmd string key with one of the values [version, load, new, config-get, config-set, call, import-paths, import-files, string-import-paths]")); 266 | } 267 | } 268 | 269 | void processException(int id, Throwable e) 270 | { 271 | stderr.writeln(e); 272 | // dfmt off 273 | sendResponse(id, JSONValue([ 274 | "error": JSONValue(true), 275 | "msg": JSONValue(e.msg), 276 | "exception": JSONValue(e.toString()) 277 | ])); 278 | // dfmt on 279 | } 280 | 281 | void processException(int id, JSONValue request, Throwable e) 282 | { 283 | stderr.writeln(e); 284 | // dfmt off 285 | sendResponse(id, JSONValue([ 286 | "error": JSONValue(true), 287 | "msg": JSONValue(e.msg), 288 | "exception": JSONValue(e.toString()), 289 | "request": request 290 | ])); 291 | // dfmt on 292 | } 293 | 294 | version (unittest) 295 | { 296 | } 297 | else 298 | { 299 | int main(string[] args) 300 | { 301 | import workspaced.info; 302 | 303 | import std.file; 304 | import etc.linux.memoryerror; 305 | 306 | version (DigitalMars) 307 | static if (is(typeof(registerMemoryErrorHandler))) 308 | registerMemoryErrorHandler(); 309 | 310 | if (args.length > 1 && (args[1] == "-v" || args[1] == "--version" || args[1] == "-version")) 311 | { 312 | stdout.writeln(getVersionInfoString); 313 | return 0; 314 | } 315 | 316 | engine = new WorkspaceD(); 317 | engine.onBroadcast = (&broadcast).toDelegate; 318 | engine.onBindFail = (&bindFail).toDelegate; 319 | scope (exit) 320 | engine.shutdown(); 321 | 322 | writeMutex = new Mutex; 323 | commandMutex = new Mutex; 324 | 325 | int length = 0; 326 | int id = 0; 327 | ubyte[4] intBuffer; 328 | ubyte[] dataBuffer; 329 | JSONValue data; 330 | 331 | int gcCollects; 332 | StopWatch gcInterval; 333 | gcInterval.start(); 334 | 335 | scope (exit) 336 | handleRequest(int.min, JSONValue(["cmd": "unload", "components": "*"])); 337 | 338 | stderr.writeln("Config files stored in ", standardPaths(StandardPath.config, "workspace-d")); 339 | 340 | while (stdin.isOpen && stdout.isOpen && !stdin.eof) 341 | { 342 | dataBuffer = stdin.rawRead(intBuffer); 343 | if (dataBuffer.length == 0) break; 344 | assert(dataBuffer.length == 4, "Unexpected buffer data"); 345 | length = bigEndianToNative!int(dataBuffer[0 .. 4]); 346 | 347 | assert(length >= 4, "Invalid request"); 348 | 349 | dataBuffer = stdin.rawRead(intBuffer); 350 | assert(dataBuffer.length == 4, "Unexpected buffer data"); 351 | id = bigEndianToNative!int(dataBuffer[0 .. 4]); 352 | 353 | dataBuffer.length = length - 4; 354 | dataBuffer = stdin.rawRead(dataBuffer); 355 | 356 | try 357 | { 358 | data = parseJSON(cast(string) dataBuffer); 359 | } 360 | catch (Exception e) 361 | { 362 | processException(id, e); 363 | } 364 | catch (AssertError e) 365 | { 366 | processException(id, e); 367 | } 368 | 369 | try 370 | { 371 | handleRequest(id, data); 372 | } 373 | catch (Exception e) 374 | { 375 | processException(id, data, e); 376 | } 377 | catch (AssertError e) 378 | { 379 | processException(id, data, e); 380 | } 381 | stdout.flush(); 382 | 383 | if (gcInterval.peek >= 1.minutes) 384 | { 385 | import core.memory : GC; 386 | 387 | auto before = GC.stats(); 388 | StopWatch gcSpeed; 389 | gcSpeed.start(); 390 | GC.collect(); 391 | gcSpeed.stop(); 392 | auto after = GC.stats(); 393 | if (before != after) 394 | stderr.writefln("GC run in %s. Freed %s bytes (%s bytes allocated, %s bytes available)", gcSpeed.peek, 395 | cast(long) before.usedSize - cast(long) after.usedSize, 396 | after.usedSize, after.freeSize); 397 | else 398 | stderr.writeln("GC run in ", gcSpeed.peek); 399 | gcInterval.reset(); 400 | 401 | gcCollects++; 402 | if (gcCollects > 5) 403 | { 404 | gcSpeed.reset(); 405 | gcSpeed.start(); 406 | GC.minimize(); 407 | gcSpeed.stop(); 408 | stderr.writeln("GC minimized in ", gcSpeed.peek); 409 | gcCollects = 0; 410 | } 411 | } 412 | } 413 | return 0; 414 | } 415 | } 416 | -------------------------------------------------------------------------------- /source/workspaced/com/moduleman.d: -------------------------------------------------------------------------------- 1 | module workspaced.com.moduleman; 2 | 3 | import dparse.ast; 4 | import dparse.lexer; 5 | import dparse.parser; 6 | import dparse.rollback_allocator; 7 | 8 | import std.algorithm; 9 | import std.array; 10 | import std.conv; 11 | import std.file; 12 | import std.format; 13 | import std.functional; 14 | import std.path; 15 | import std.string; 16 | 17 | import workspaced.api; 18 | 19 | @component("moduleman") 20 | class ModulemanComponent : ComponentWrapper 21 | { 22 | mixin DefaultComponentWrapper; 23 | 24 | protected void load() 25 | { 26 | config.stringBehavior = StringBehavior.source; 27 | } 28 | 29 | /// Renames a module to something else (only in the project root). 30 | /// Params: 31 | /// renameSubmodules: when `true`, this will rename submodules of the module too. For example when renaming `lib.com` to `lib.coms` this will also rename `lib.com.*` to `lib.coms.*` 32 | /// Returns: all changes that need to happen to rename the module. If no module statement could be found this will return an empty array. 33 | FileChanges[] rename(string mod, string rename, bool renameSubmodules = true) 34 | { 35 | if (!refInstance) 36 | throw new Exception("moduleman.rename requires to be instanced"); 37 | 38 | RollbackAllocator rba; 39 | FileChanges[] changes; 40 | bool foundModule = false; 41 | auto from = mod.split('.'); 42 | auto to = rename.split('.'); 43 | foreach (file; dirEntries(instance.cwd, SpanMode.depth)) 44 | { 45 | if (file.extension != ".d") 46 | continue; 47 | string code = readText(file); 48 | auto tokens = getTokensForParser(cast(ubyte[]) code, config, &workspaced.stringCache); 49 | auto parsed = parseModule(tokens, file, &rba); 50 | auto reader = new ModuleChangerVisitor(file, from, to, renameSubmodules); 51 | reader.visit(parsed); 52 | if (reader.changes.replacements.length) 53 | changes ~= reader.changes; 54 | if (reader.foundModule) 55 | foundModule = true; 56 | } 57 | if (!foundModule) 58 | return []; 59 | return changes; 60 | } 61 | 62 | /// Renames/adds/removes a module from a file to match the majority of files in the folder. 63 | /// Params: 64 | /// file: File path to the file to normalize 65 | /// code: Current code inside the text buffer 66 | CodeReplacement[] normalizeModules(scope const(char)[] file, scope const(char)[] code) 67 | { 68 | if (!refInstance) 69 | throw new Exception("moduleman.normalizeModules requires to be instanced"); 70 | 71 | int[string] modulePrefixes; 72 | modulePrefixes[""] = 0; 73 | auto modName = file.replace("\\", "/").stripExtension; 74 | if (modName.baseName == "package") 75 | modName = modName.dirName; 76 | if (modName.startsWith(instance.cwd.replace("\\", "/"))) 77 | modName = modName[instance.cwd.length .. $]; 78 | modName = modName.stripLeft('/'); 79 | auto longest = modName; 80 | foreach (imp; importPaths) 81 | { 82 | imp = imp.replace("\\", "/"); 83 | if (imp.startsWith(instance.cwd.replace("\\", "/"))) 84 | imp = imp[instance.cwd.length .. $]; 85 | imp = imp.stripLeft('/'); 86 | if (longest.startsWith(imp)) 87 | { 88 | auto shortened = longest[imp.length .. $]; 89 | if (shortened.length < modName.length) 90 | modName = shortened; 91 | } 92 | } 93 | auto sourcePos = (modName ~ '/').indexOf("/source/"); 94 | if (sourcePos != -1) 95 | modName = modName[sourcePos + "/source".length .. $]; 96 | modName = modName.stripLeft('/').replace("/", "."); 97 | if (!modName.length) 98 | return []; 99 | auto existing = describeModule(code); 100 | if (modName == existing.moduleName) 101 | { 102 | return []; 103 | } 104 | else 105 | { 106 | if (modName == "") 107 | { 108 | return [CodeReplacement([existing.outerFrom, existing.outerTo], "")]; 109 | } 110 | else 111 | { 112 | const trailing = code[min(existing.outerTo, $) .. $]; 113 | // determine number of new lines to insert after full module name + semicolon 114 | string semicolonNewlines = trailing.startsWith("\n\n", "\r\r", "\r\n\r\n") ? ";" 115 | : trailing.startsWith("\n", "\r") ? ";\n" 116 | : ";\n\n"; 117 | return [ 118 | CodeReplacement([existing.outerFrom, existing.outerTo], text("module ", 119 | modName, (existing.outerTo == existing.outerFrom ? semicolonNewlines : ";"))) 120 | ]; 121 | } 122 | } 123 | } 124 | 125 | /// Returns the module name parts of a D code 126 | const(string)[] getModule(scope const(char)[] code) 127 | { 128 | return describeModule(code).raw; 129 | } 130 | 131 | /// Returns the normalized module name as string of a D code 132 | string moduleName(scope const(char)[] code) 133 | { 134 | return describeModule(code).moduleName; 135 | } 136 | 137 | /// 138 | FileModuleInfo describeModule(scope const(char)[] code) 139 | { 140 | auto tokens = getTokensForParser(cast(ubyte[]) code, config, &workspaced.stringCache); 141 | ptrdiff_t start = -1; 142 | size_t from, to; 143 | size_t outerFrom, outerTo; 144 | 145 | foreach (i, Token t; tokens) 146 | { 147 | if (t.type == tok!"module") 148 | { 149 | start = i; 150 | outerFrom = t.index; 151 | break; 152 | } 153 | } 154 | 155 | if (start == -1) 156 | { 157 | FileModuleInfo ret; 158 | start = 0; 159 | if (tokens.length && tokens[0].type == tok!"scriptLine") 160 | { 161 | start = code.indexOfAny("\r\n"); 162 | if (start == -1) 163 | { 164 | start = 0; 165 | } 166 | else 167 | { 168 | if (start + 1 < code.length && code[start] == '\r' && code[start + 1] == '\n') 169 | start += 2; 170 | else 171 | start++; 172 | // support /+ dub.sdl: lines or similar starts directly after a script line 173 | auto leading = code[start .. $].stripLeft; 174 | if (leading.startsWith("/+", "/*")) 175 | { 176 | size_t end = code.indexOf(leading[1] == '+' ? "+/" : "*/", start); 177 | if (end != -1) 178 | { 179 | start = end + 2; 180 | if (code[start .. $].startsWith("\r\n")) 181 | start += 2; 182 | else if (code[start .. $].startsWith("\r", "\n")) 183 | start += 1; 184 | } 185 | } 186 | } 187 | } 188 | ret.outerFrom = ret.outerTo = ret.from = ret.to = start; 189 | return ret; 190 | } 191 | 192 | const(string)[] raw; 193 | string moduleName; 194 | foreach (t; tokens[start + 1 .. $]) 195 | { 196 | if (t.type == tok!";") 197 | { 198 | outerTo = t.index + 1; 199 | break; 200 | } 201 | if (t.type == tok!"identifier") 202 | { 203 | if (from == 0) 204 | from = t.index; 205 | moduleName ~= t.text; 206 | to = t.index + t.text.length; 207 | raw ~= t.text; 208 | } 209 | if (t.type == tok!".") 210 | { 211 | moduleName ~= "."; 212 | } 213 | } 214 | return FileModuleInfo(raw, moduleName, from, to, outerFrom, outerTo); 215 | } 216 | 217 | private: 218 | LexerConfig config; 219 | } 220 | 221 | /// Represents a module statement in a file. 222 | struct FileModuleInfo 223 | { 224 | /// Parts of the module name as array. 225 | const(string)[] raw; 226 | /// Whole modulename as normalized string in form a.b.c etc. 227 | string moduleName = ""; 228 | /// Code index of the moduleName 229 | size_t from, to; 230 | /// Code index of the whole module statement starting right at module and ending right after the semicolon. 231 | size_t outerFrom, outerTo; 232 | 233 | string toString() const 234 | { 235 | return format!"FileModuleInfo[%s..%s](name[%s..%s]=%s)"(outerFrom, outerTo, from, to, moduleName); 236 | } 237 | } 238 | 239 | private: 240 | 241 | class ModuleChangerVisitor : ASTVisitor 242 | { 243 | this(string file, string[] from, string[] to, bool renameSubmodules) 244 | { 245 | changes.file = file; 246 | this.from = from; 247 | this.to = to; 248 | this.renameSubmodules = renameSubmodules; 249 | } 250 | 251 | alias visit = ASTVisitor.visit; 252 | 253 | override void visit(const ModuleDeclaration decl) 254 | { 255 | auto mod = decl.moduleName.identifiers.map!(a => a.text).array; 256 | auto orig = mod; 257 | if (mod.startsWith(from) && renameSubmodules) 258 | mod = to ~ mod[from.length .. $]; 259 | else if (mod == from) 260 | mod = to; 261 | if (mod != orig) 262 | { 263 | foundModule = true; 264 | changes.replacements ~= CodeReplacement([ 265 | decl.moduleName.identifiers[0].index, 266 | decl.moduleName.identifiers[$ - 1].index + decl.moduleName.identifiers[$ - 1].text.length 267 | ], mod.join('.')); 268 | } 269 | } 270 | 271 | override void visit(const SingleImport imp) 272 | { 273 | auto mod = imp.identifierChain.identifiers.map!(a => a.text).array; 274 | auto orig = mod; 275 | if (mod.startsWith(from) && renameSubmodules) 276 | mod = to ~ mod[from.length .. $]; 277 | else if (mod == from) 278 | mod = to; 279 | if (mod != orig) 280 | { 281 | changes.replacements ~= CodeReplacement([ 282 | imp.identifierChain.identifiers[0].index, 283 | imp.identifierChain.identifiers[$ - 1].index 284 | + imp.identifierChain.identifiers[$ - 1].text.length 285 | ], mod.join('.')); 286 | } 287 | } 288 | 289 | override void visit(const ImportDeclaration decl) 290 | { 291 | if (decl) 292 | { 293 | return decl.accept(this); 294 | } 295 | } 296 | 297 | override void visit(const BlockStatement content) 298 | { 299 | if (content) 300 | { 301 | return content.accept(this); 302 | } 303 | } 304 | 305 | string[] from, to; 306 | FileChanges changes; 307 | bool renameSubmodules, foundModule; 308 | } 309 | 310 | unittest 311 | { 312 | scope backend = new WorkspaceD(); 313 | auto workspace = makeTemporaryTestingWorkspace; 314 | workspace.createDir("source/newmod"); 315 | workspace.createDir("unregistered/source"); 316 | workspace.writeFile("source/newmod/color.d", "module oldmod.color;void foo(){}"); 317 | workspace.writeFile("source/newmod/render.d", "module oldmod.render;import std.color,oldmod.color;import oldmod.color.oldmod:a=b, c;import a=oldmod.a;void bar(){}"); 318 | workspace.writeFile("source/newmod/display.d", "module newmod.displaf;"); 319 | workspace.writeFile("source/newmod/input.d", ""); 320 | workspace.writeFile("source/newmod/package.d", ""); 321 | workspace.writeFile("unregistered/source/package.d", ""); 322 | workspace.writeFile("unregistered/source/app.d", ""); 323 | auto instance = backend.addInstance(workspace.directory); 324 | backend.register!ModulemanComponent; 325 | auto mod = backend.get!ModulemanComponent(workspace.directory); 326 | 327 | instance.importPathProvider = () => ["source", "source/deeply/nested/source"]; 328 | 329 | FileChanges[] changes = mod.rename("oldmod", "newmod").sort!"a.file < b.file".array; 330 | 331 | assert(changes.length == 2); 332 | assert(changes[0].file.endsWith("color.d")); 333 | assert(changes[1].file.endsWith("render.d")); 334 | 335 | assert(changes[0].replacements == [CodeReplacement([7, 19], "newmod.color")]); 336 | assert(changes[1].replacements == [ 337 | CodeReplacement([7, 20], "newmod.render"), 338 | CodeReplacement([38, 50], "newmod.color"), 339 | CodeReplacement([58, 77], "newmod.color.oldmod"), 340 | CodeReplacement([94, 102], "newmod.a") 341 | ]); 342 | 343 | foreach (change; changes) 344 | { 345 | string code = readText(change.file); 346 | foreach_reverse (op; change.replacements) 347 | code = op.apply(code); 348 | std.file.write(change.file, code); 349 | } 350 | 351 | auto nrm = mod.normalizeModules(workspace.getPath("source/newmod/input.d"), ""); 352 | assert(nrm == [CodeReplacement([0, 0], "module newmod.input;\n\n")]); 353 | 354 | nrm = mod.normalizeModules(workspace.getPath("source/newmod/package.d"), ""); 355 | assert(nrm == [CodeReplacement([0, 0], "module newmod;\n\n")]); 356 | 357 | nrm = mod.normalizeModules(workspace.getPath("source/newmod/display.d"), 358 | "module oldmod.displaf;"); 359 | assert(nrm == [CodeReplacement([0, 22], "module newmod.display;")]); 360 | 361 | nrm = mod.normalizeModules(workspace.getPath("unregistered/source/app.d"), ""); 362 | assert(nrm == [CodeReplacement([0, 0], "module app;\n\n")]); 363 | 364 | nrm = mod.normalizeModules(workspace.getPath("unregistered/source/package.d"), ""); 365 | assert(nrm == []); 366 | 367 | nrm = mod.normalizeModules(workspace.getPath("source/deeply/nested/source/pkg/test.d"), ""); 368 | assert(nrm == [CodeReplacement([0, 0], "module pkg.test;\n\n")]); 369 | 370 | auto fetched = mod.describeModule("/* hello world */ module\nfoo . \nbar ;\n\nvoid foo() {"); 371 | assert(fetched == FileModuleInfo(["foo", "bar"], "foo.bar", 25, 35, 18, 38)); 372 | 373 | fetched = mod.describeModule(`#!/usr/bin/env dub 374 | /+ dub.sdl: 375 | name "hello" 376 | +/ 377 | void main() {}`); 378 | assert(fetched == FileModuleInfo([], "", 48, 48, 48, 48)); 379 | 380 | fetched = mod.describeModule("#!/usr/bin/rdmd\r\n"); 381 | assert(fetched == FileModuleInfo([], "", 17, 17, 17, 17)); 382 | 383 | fetched = mod.describeModule("#!/usr/bin/rdmd\n"); 384 | assert(fetched == FileModuleInfo([], "", 16, 16, 16, 16)); 385 | } 386 | -------------------------------------------------------------------------------- /views/dml-completion.txt: -------------------------------------------------------------------------------- 1 | // enums 2 | enum FontFamily --- font families enum 3 | { 4 | Cursive; --- Cursive font 5 | Fantasy; --- Fantasy font 6 | MonoSpace; --- Monospace font (fixed pitch font), e.g. Courier New 7 | SansSerif; --- Sans Serif font, e.g. Arial 8 | Serif; --- Serif font, e.g. Times New Roman 9 | Unspecified; --- Unknown / not set / does not matter 10 | } 11 | enum Visibility --- Visibility (see Android View Visibility) 12 | { 13 | Gone; --- Completely hidden, as not has been added 14 | Invisible; --- Not visible, but occupies a space in layout 15 | Visible; --- Visible on screen (default) 16 | } 17 | enum Align --- Align option bit constants 18 | { 19 | Bottom; --- vertically align to the bottom of box 20 | Center; --- align to the center of box (VCenter | HCenter) 21 | HCenter; --- horizontally align to the center of box 22 | Left; --- horizontally align to the left of box 23 | Right; --- horizontally align to the right of box 24 | Top; --- vertically align to the top of box 25 | TopLeft; --- align to the top left corner of box (Left | Top) 26 | Unspecified; --- alignment is not specified 27 | VCenter; --- vertically align to the center of box 28 | } 29 | enum State --- Widget state flags - bits 30 | { 31 | Activated; --- widget is activated 32 | Checkable; --- widget can be checked 33 | Checked; --- widget is checked 34 | Default; --- widget is default control for form (should be focused when window gains focus first time) 35 | Enabled; --- widget can process mouse and key events 36 | Focused; --- widget has focus 37 | Hovered; --- mouse pointer is over this widget 38 | Normal; --- state not specified / normal 39 | Parent; --- return state of parent instead of widget's state when requested 40 | Pressed; --- pressed (e.g. clicked by mouse) 41 | Selected; --- widget is selected 42 | WindowFocused; --- window is focused 43 | } 44 | enum TextFlag --- text drawing flag bits 45 | { 46 | HotKeys; --- text contains hot key prefixed with & char (e.g. "&File") 47 | StrikeThrough; --- strikethrough text when drawing 48 | Underline; --- underline text when drawing 49 | UnderlineHotKeys; --- underline hot key when drawing 50 | UnderlineHotKeysWhenAltPressed; --- underline hot key when drawing 51 | } 52 | enum DockAlignment 53 | { 54 | Bottom; 55 | Left; 56 | Right; 57 | Top; 58 | } 59 | enum Orientation 60 | { 61 | Horizontal; 62 | Vertical; 63 | } 64 | enum PopupFlags --- popup behavior flags - for PopupWidget.flags property 65 | { 66 | CloseOnClickOutside; --- close popup when mouse button clicked outside of its bounds 67 | } 68 | // widget 69 | class Widget --- Base class for all widgets. 70 | { 71 | Align alignment; --- sets alignment (combined vertical and horizontal) 72 | number alpha; --- set widget drawing alpha value (0=opaque .. 255=transparent) 73 | color backgroundColor; --- set background color for widget - override one from style 74 | string backgroundImageId; --- background image id 75 | bool checkable; --- when true, control supports Checked state 76 | bool checked; --- set checked state 77 | bool clickable; --- when true, user can click this control, and get onClick listeners called 78 | bool enabled; --- change enabled state 79 | bool focusable; --- whether widget can be focused 80 | bool focusGroup; --- When focus group is set for some parent widget, focus from one of containing widgets can be moved using keyboard only to one of other widgets containing in it and cannot bypass bounds of focusGroup. 81 | string fontFace; --- set font face for widget - override one from style 82 | FontFamily fontFamily; --- set font family for widget - override one from style 83 | bool fontItalic; --- set font style (italic/normal) for widget - override one from style 84 | number fontSize; --- font size in pixels 85 | number fontWeight; --- set font weight for widget - override one from style 86 | string id; --- set widget id 87 | number layoutHeight; --- sets layout height options (WRAP_CONTENT, FILL_PARENT, or some constant value) 88 | number layoutWeight; --- sets layout weight (while resizing to fill parent, widget will be resized proportionally to this value) 89 | number layoutWidth; --- sets layout width options (WRAP_CONTENT, FILL_PARENT, or some constant value) 90 | rect margins; --- margins (between widget bounds and its background) 91 | number maxHeight; --- set max height constraint (SIZE_UNSPECIFIED for no constraint) 92 | number maxWidth; --- set max width constraint (SIZE_UNSPECIFIED for no constraint) 93 | number minHeight; --- set min height constraint (0 for no constraint) 94 | number minWidth; --- set min width constraint (0 for no constraint) 95 | rect padding; --- padding (between background bounds and content of widget) 96 | State resetState; --- remove state flags 97 | State setState; --- add state flags 98 | State state; --- set new widget state 99 | string styleId; --- widget style id, null if not set 100 | number tabOrder; --- tab order - hint for focus movement using Tab/Shift+Tab 101 | string text; --- sets widget content text (override to support this) 102 | color textColor; --- set text color (ARGB 32 bit value) 103 | TextFlag textFlags; --- set text flags 104 | bool trackHover; --- mouse movement processing flag 105 | Visibility visibility; --- sets widget visibility (Visible, Invisible, Gone) 106 | } 107 | class WidgetGroup --- Base class for widgets which have children. 108 | { 109 | include Widget; 110 | } 111 | class WidgetGroupDefaultDrawing --- WidgetGroup with default drawing of children (just draw all children) 112 | { 113 | include Widget; 114 | } 115 | // appframe 116 | class AppFrame 117 | { 118 | include VerticalLayout; 119 | } 120 | // combobox 121 | class ComboBoxBase --- Abstract ComboBox 122 | { 123 | number selectedItemIndex; --- Selected item index 124 | include HorizontalLayout; 125 | } 126 | class ComboBox --- ComboBox with list of strings 127 | { 128 | include ComboBoxBase; 129 | } 130 | class ComboEdit --- Editable ComboBox with list of strings 131 | { 132 | bool readOnly; 133 | include ComboBox; 134 | } 135 | // controls 136 | class AbstractSlider --- base class for widgets like scrollbars and sliders 137 | { 138 | number pageSize; --- set page size (visible area size) 139 | number position; --- sets new slider position 140 | include WidgetGroup; 141 | } 142 | class Button --- Text only button 143 | { 144 | string textResource; 145 | include Widget; 146 | } 147 | class CheckBox --- checkbox 148 | { 149 | include ImageTextButton; 150 | } 151 | class HSpacer --- horizontal spacer to fill empty space in horizontal layouts 152 | { 153 | include Widget; 154 | } 155 | class ImageButton --- button with image only 156 | { 157 | include ImageWidget; 158 | } 159 | class ImageTextButton --- button with image and text 160 | { 161 | include HorizontalLayout; 162 | } 163 | class ImageWidget --- static image widget 164 | { 165 | string drawable; --- set custom drawable (not one from resources) 166 | string drawableId; --- set drawable image id 167 | include Widget; 168 | } 169 | class RadioButton --- radio button 170 | { 171 | include ImageTextButton; 172 | } 173 | class ScrollBar --- scroll bar - either vertical or horizontal 174 | { 175 | include AbstractSlider; 176 | } 177 | class TextWidget --- static text widget 178 | { 179 | string textResource; --- set text resource ID to show 180 | include Widget; 181 | } 182 | class VSpacer --- vertical spacer to fill empty space in vertical layouts 183 | { 184 | include Widget; 185 | } 186 | // docks 187 | class DockHost --- Layout for docking support - contains body widget and optional docked windows 188 | { 189 | include WidgetGroupDefaultDrawing; 190 | } 191 | class DockWindow --- docked window 192 | { 193 | DockAlignment dockAlignment; 194 | include VerticalLayout; 195 | } 196 | // editors 197 | class EditBox --- multiline editor 198 | { 199 | number maxFontSize; 200 | number minFontSize; 201 | include EditWidgetBase; 202 | } 203 | class EditLine --- single line editor 204 | { 205 | include EditWidgetBase; 206 | } 207 | class EditWidgetBase --- base for all editor widgets 208 | { 209 | bool readOnly; --- readonly flag (when true, user cannot change content of editor) 210 | bool replaceMode; --- replace mode flag (when true, entered character replaces character under cursor) 211 | bool showLineNumbers; --- when true, line numbers are shown 212 | number tabSize; --- sets tab size (in number of spaces) 213 | bool useSpacesForTabs; --- when true, spaces will be inserted instead of tabs 214 | bool wantTabs; --- when true, Tab / Shift+Tab presses are processed internally in widget (e.g. insert tab character) instead of focus change navigation 215 | include ScrollWidgetBase; 216 | } 217 | // grid 218 | class GridWidgetBase --- Abstract grid widget 219 | { 220 | number cols; --- set column count 221 | number defColumnWidth; --- default column width - for newly added columns 222 | number defRowHeight; --- default row height - for newly added columns 223 | number fixedCols; --- fixed (non-scrollable) data column count 224 | number fixedRows; --- fixed (non-scrollable) data row count 225 | number headerCols; --- row header column count 226 | number headerRows; --- col header row count 227 | number rows; --- set row count 228 | bool rowSelect; --- when true, allows only select the whole row 229 | bool showColHeaders; --- flag to enable column headers 230 | bool showRowHeaders; --- flag to enable row headers 231 | include ScrollWidgetBase; 232 | } 233 | class StringGridWidget --- Grid view with string data shown. All rows are of the same height 234 | { 235 | include StringGridWidgetBase; 236 | } 237 | class StringGridWidgetBase 238 | { 239 | include GridWidgetBase; 240 | } 241 | // layouts 242 | class FrameLayout --- place all children into same place (usually, only one child should be visible at a time) 243 | { 244 | include WidgetGroupDefaultDrawing; 245 | } 246 | class HorizontalLayout --- Arranges children horizontally 247 | { 248 | include LinearLayout; 249 | } 250 | class LinearLayout --- Arranges items either vertically or horizontally 251 | { 252 | Orientation orientation; --- sets linear layout orientation 253 | include Widget; 254 | } 255 | class ResizerWidget --- Resizer control. Put it between other items in LinearLayout to allow resizing its siblings. While dragging, it will resize previous and next children in layout 256 | { 257 | include Widget; 258 | } 259 | class TableLayout --- layout children as table with rows and columns 260 | { 261 | number colCount; --- number of columns 262 | include WidgetGroupDefaultDrawing; 263 | } 264 | class VerticalLayout --- Arranges children vertically 265 | { 266 | include LinearLayout; 267 | } 268 | // lists 269 | class ListWidget --- List widget - shows content as hori 270 | { 271 | Orientation orientation; --- sets linear layout orientation 272 | number selectedItemIndex; --- Selected item index 273 | bool selectOnHover; --- when true, mouse hover selects underlying item 274 | include WidgetGroup; 275 | } 276 | // menu 277 | class MainMenu --- main menu (horizontal) 278 | { 279 | include MenuWidgetBase; 280 | } 281 | class MenuItemWidget --- widget to draw menu item 282 | { 283 | include WidgetGroupDefaultDrawing; 284 | } 285 | class MenuWidgetBase --- base class for menus 286 | { 287 | include ListWidget; 288 | } 289 | class PopupMenu --- popup menu widget (vertical layout of items) 290 | { 291 | include MenuWidgetBase; 292 | } 293 | // popup 294 | class PopupWidget --- popup widget container 295 | { 296 | PopupFlags flags; --- set popup behavior flags 297 | include LinearLayout; 298 | } 299 | // scroll 300 | class ScrollWidget --- Widget which can show content of widget group with optional scrolling 301 | { 302 | include ScrollWidgetBase; 303 | } 304 | class ScrollWidgetBase --- Abstract scrollable widget 305 | { 306 | include WidgetGroup; 307 | } 308 | // srcedit 309 | class SourceEdit 310 | { 311 | include EditBox; 312 | } 313 | // statusline 314 | class StatusLine --- Status line control 315 | { 316 | include HorizontalLayout; 317 | } 318 | // styles 319 | // tabs 320 | class TabControl --- tab header - tab labels, with optional More button 321 | { 322 | include WidgetGroupDefaultDrawing; 323 | } 324 | class TabHost --- container for widgets controlled by TabControl 325 | { 326 | include FrameLayout; 327 | } 328 | class TabItemWidget --- tab item widget - to show tab header 329 | { 330 | include HorizontalLayout; 331 | } 332 | class TabWidget --- compound widget - contains from TabControl widget (tabs header) and TabHost (content pages) 333 | { 334 | include VerticalLayout; 335 | } 336 | // toolbars 337 | class ToolBar --- Layout with buttons 338 | { 339 | include HorizontalLayout; 340 | } 341 | class ToolBarHost --- Layout with several toolbars 342 | { 343 | include HorizontalLayout; 344 | } 345 | class ToolBarImageButton --- image button for toolbar 346 | { 347 | include ImageButton; 348 | } 349 | class ToolBarSeparator --- separator for toolbars 350 | { 351 | include ImageWidget; 352 | } 353 | // tree 354 | class TreeItemWidget --- Item widget for displaying in trees 355 | { 356 | include HorizontalLayout; 357 | } 358 | class TreeWidget --- Tree widget with items which can have icons and labels 359 | { 360 | include TreeWidgetBase; 361 | } 362 | class TreeWidgetBase --- Abstract tree widget 363 | { 364 | include ScrollWidget; 365 | } -------------------------------------------------------------------------------- /source/workspaced/visitors/methodfinder.d: -------------------------------------------------------------------------------- 1 | /// Finds methods in a specified interface or class location. 2 | module workspaced.visitors.methodfinder; 3 | 4 | import workspaced.visitors.attributes; 5 | 6 | import workspaced.dparseext; 7 | 8 | import dparse.ast; 9 | import dparse.formatter; 10 | import dparse.lexer; 11 | 12 | import std.algorithm; 13 | import std.array; 14 | import std.range; 15 | import std.string; 16 | 17 | /// Information about an argument in a method defintion. 18 | struct ArgumentInfo 19 | { 20 | /// The whole definition of the argument including everything related to it as formatted code string. 21 | string signature; 22 | /// The type of the argument. 23 | string type; 24 | /// The name of the argument. 25 | string name; 26 | 27 | /// Returns just the name. 28 | string toString() const 29 | { 30 | return name; 31 | } 32 | } 33 | 34 | /// Information about a method definition. 35 | struct MethodDetails 36 | { 37 | /// The name of the method. 38 | string name; 39 | /// The type definition of the method without body, abstract or final. 40 | string signature; 41 | /// The return type of the method. 42 | string returnType; 43 | /// All (regular) arguments passable into this method. 44 | ArgumentInfo[] arguments; 45 | /// 46 | bool isNothrowOrNogc; 47 | /// True if this function has an implementation. 48 | bool hasBody; 49 | /// True when the container is an interface or (optionally implicit) abstract class or when in class not having a body. 50 | bool needsImplementation; 51 | /// True when in a class and method doesn't have a body. 52 | bool optionalImplementation; 53 | /// Range starting at return type, going until last token before opening curly brace. 54 | size_t[2] definitionRange; 55 | /// Range containing the starting and ending braces of the body. 56 | size_t[2] blockRange; 57 | 58 | /// Signature without any attributes, constraints or parameter details other than types. 59 | /// Used to differentiate a method from others without computing the mangle. 60 | /// Returns: `" ()"` 61 | string identifier() 62 | { 63 | return format("%s %s(%(%s,%))", returnType, name, arguments.map!"a.type"); 64 | } 65 | } 66 | 67 | /// 68 | struct FieldDetails 69 | { 70 | /// 71 | string name, type; 72 | /// 73 | bool isPrivate; 74 | } 75 | 76 | /// 77 | struct TypeDetails 78 | { 79 | enum Type 80 | { 81 | none, 82 | class_, 83 | interface_, 84 | enum_, 85 | struct_, 86 | union_, 87 | alias_, 88 | template_, 89 | } 90 | 91 | /// Name in last element, all parents in previous elements. 92 | string[] name; 93 | /// 94 | size_t nameLocation; 95 | /// 96 | Type type; 97 | } 98 | 99 | /// 100 | struct ReferencedType 101 | { 102 | /// Referenced type name, might be longer than actual written name because of normalization of parents. 103 | string name; 104 | /// Location of name which will start right before the last identifier of the type in a dot chain. 105 | size_t location; 106 | } 107 | 108 | /// Information about an interface or class 109 | struct InterfaceDetails 110 | { 111 | /// Entire code of the file 112 | const(char)[] code; 113 | /// True if this is a class and therefore need to override methods using $(D override). 114 | bool needsOverride; 115 | /// Name of the interface or class. 116 | string name; 117 | /// Plain old variable fields in this container. 118 | FieldDetails[] fields; 119 | /// All methods defined in this container. 120 | MethodDetails[] methods; 121 | /// A list of nested types and locations defined in this interface/class. 122 | TypeDetails[] types; 123 | // reserved for future use with templates 124 | string[] parents; 125 | /// Name of all base classes or interfaces. Should use normalizedParents, 126 | string[] normalizedParents; 127 | /// Absolute code position after the colon where the corresponding parent name starts. 128 | int[] parentPositions; 129 | /// Range containing the starting and ending braces of the body. 130 | size_t[2] blockRange; 131 | /// A (name-based) sorted set of referenced types with first occurences of every type or alias not including built-in types, but including object.d types and aliases. 132 | ReferencedType[] referencedTypes; 133 | 134 | /// Returns true if there are no non-whitespace characters inside the block. 135 | bool isEmpty() const 136 | { 137 | return !code.substr(blockRange).strip.length; 138 | } 139 | } 140 | 141 | class InterfaceMethodFinder : AttributesVisitor 142 | { 143 | this(const(char)[] code, int targetPosition) 144 | { 145 | this.code = code; 146 | details.code = code; 147 | this.targetPosition = targetPosition; 148 | } 149 | 150 | override void visit(const StructDeclaration dec) 151 | { 152 | if (inTarget) 153 | return; 154 | else 155 | super.visit(dec); 156 | } 157 | 158 | override void visit(const UnionDeclaration dec) 159 | { 160 | if (inTarget) 161 | return; 162 | else 163 | super.visit(dec); 164 | } 165 | 166 | override void visit(const EnumDeclaration dec) 167 | { 168 | if (inTarget) 169 | return; 170 | else 171 | super.visit(dec); 172 | } 173 | 174 | override void visit(const ClassDeclaration dec) 175 | { 176 | if (inTarget) 177 | return; 178 | 179 | auto c = context.save(); 180 | context.pushContainer(ASTContext.ContainerAttribute.Type.class_, dec.name.text); 181 | visitInterface(dec.name, dec.baseClassList, dec.structBody, true); 182 | context.restore(c); 183 | } 184 | 185 | override void visit(const InterfaceDeclaration dec) 186 | { 187 | if (inTarget) 188 | return; 189 | 190 | auto c = context.save(); 191 | context.pushContainer(ASTContext.ContainerAttribute.Type.interface_, dec.name.text); 192 | visitInterface(dec.name, dec.baseClassList, dec.structBody, false); 193 | context.restore(c); 194 | } 195 | 196 | private void visitInterface(const Token name, const BaseClassList baseClassList, 197 | const StructBody structBody, bool needsOverride) 198 | { 199 | if (!structBody) 200 | return; 201 | if (inTarget) 202 | return; // ignore nested 203 | 204 | if (targetPosition >= name.index && targetPosition < structBody.endLocation) 205 | { 206 | details.blockRange = [structBody.startLocation, structBody.endLocation + 1]; 207 | details.name = name.text; 208 | if (baseClassList) 209 | foreach (base; baseClassList.items) 210 | { 211 | if (!base.type2 || !base.type2.typeIdentifierPart 212 | || !base.type2.typeIdentifierPart.identifierOrTemplateInstance) 213 | continue; 214 | // TODO: template support! 215 | details.parents ~= astToString(base.type2); 216 | details.normalizedParents ~= astToString(base.type2); 217 | details.parentPositions ~= cast( 218 | int) base.type2.typeIdentifierPart.identifierOrTemplateInstance.identifier.index + 1; 219 | } 220 | details.needsOverride = needsOverride; 221 | inTarget = true; 222 | structBody.accept(new NestedTypeFinder(&details, details.name)); 223 | super.visit(structBody); 224 | inTarget = false; 225 | } 226 | } 227 | 228 | override void visit(const FunctionDeclaration dec) 229 | { 230 | if (!inTarget) 231 | return; 232 | 233 | size_t[2] definitionRange = [dec.name.index, 0]; 234 | size_t[2] blockRange; 235 | 236 | if (dec.returnType !is null && dec.returnType.tokens.length > 0) 237 | definitionRange[0] = dec.returnType.tokens[0].index; 238 | 239 | if (dec.functionBody !is null && dec.functionBody.tokens.length > 0) 240 | { 241 | definitionRange[1] = dec.functionBody.tokens[0].index; 242 | blockRange = [ 243 | dec.functionBody.tokens[0].index, dec.functionBody.tokens[$ - 1].index + 1 244 | ]; 245 | } 246 | else if (dec.parameters !is null && dec.parameters.tokens.length > 0) 247 | definitionRange[1] = dec.parameters.tokens[$ - 1].index 248 | + dec.parameters.tokens[$ - 1].text.length; 249 | 250 | auto origBody = (cast() dec).functionBody; 251 | const hasBody = !!origBody && origBody.missingFunctionBody is null; 252 | auto origComment = (cast() dec).comment; 253 | const implLevel = context.requiredImplementationLevel; 254 | const optionalImplementation = implLevel == 1 && !hasBody; 255 | const needsImplementation = implLevel == 9 || optionalImplementation; 256 | (cast() dec).functionBody = null; 257 | (cast() dec).comment = null; 258 | scope (exit) 259 | { 260 | (cast() dec).functionBody = origBody; 261 | (cast() dec).comment = origComment; 262 | } 263 | auto t = appender!string; 264 | formatTypeTransforming(t, dec, &resolveType); 265 | string method = context.localFormattedAttributes.chain([t.data.strip]) 266 | .filter!(a => a.length > 0 && !a.among!("abstract", "final")).join(" "); 267 | ArgumentInfo[] arguments; 268 | if (dec.parameters) 269 | foreach (arg; dec.parameters.parameters) 270 | arguments ~= ArgumentInfo(astToString(arg), astToString(arg.type), arg.name.text); 271 | string returnType = dec.returnType ? resolveType(astToString(dec.returnType)) : "void"; 272 | 273 | // now visit to populate isNothrow, isNogc (before it would add to the localFormattedAttributes string) 274 | // also fills in used types 275 | super.visit(dec); 276 | 277 | details.methods ~= MethodDetails(dec.name.text, method, returnType, arguments, context.isNothrowInContainer 278 | || context.isNogcInContainer, hasBody, needsImplementation, 279 | optionalImplementation, definitionRange, blockRange); 280 | } 281 | 282 | override void visit(const FunctionBody) 283 | { 284 | } 285 | 286 | override void visit(const VariableDeclaration variable) 287 | { 288 | if (!inTarget) 289 | return; 290 | if (!variable.type) 291 | return; 292 | string type = astToString(variable.type); 293 | auto isPrivate = context.protectionType == tok!"private"; 294 | 295 | foreach (decl; variable.declarators) 296 | details.fields ~= FieldDetails(decl.name.text, type, isPrivate); 297 | 298 | if (variable.type) 299 | variable.type.accept(this); // to fill in types 300 | } 301 | 302 | override void visit(const TypeIdentifierPart type) 303 | { 304 | if (!inTarget) 305 | return; 306 | 307 | if (type.identifierOrTemplateInstance && !type.typeIdentifierPart) 308 | { 309 | auto tok = type.identifierOrTemplateInstance.templateInstance 310 | ? type.identifierOrTemplateInstance.templateInstance.identifier 311 | : type.identifierOrTemplateInstance.identifier; 312 | 313 | usedType(ReferencedType(tok.text, tok.index)); 314 | } 315 | 316 | super.visit(type); 317 | } 318 | 319 | alias visit = AttributesVisitor.visit; 320 | 321 | protected void usedType(ReferencedType type) 322 | { 323 | // this is a simple sorted set insert 324 | auto sorted = assumeSorted!"a.name < b.name"(details.referencedTypes).trisect(type); 325 | if (sorted[1].length) 326 | return; // exists already 327 | details.referencedTypes.insertInPlace(sorted[0].length, type); 328 | } 329 | 330 | string resolveType(const(char)[] inType) 331 | { 332 | auto parts = inType.splitter('.'); 333 | string[] best; 334 | foreach (type; details.types) 335 | if ((!best.length || type.name.length < best.length) && type.name.endsWith(parts)) 336 | best = type.name; 337 | 338 | if (best.length) 339 | return best.join("."); 340 | else 341 | return inType.idup; 342 | } 343 | 344 | const(char)[] code; 345 | bool inTarget; 346 | int targetPosition; 347 | InterfaceDetails details; 348 | } 349 | 350 | class NestedTypeFinder : ASTVisitor 351 | { 352 | this(InterfaceDetails* details, string start) 353 | { 354 | this.details = details; 355 | this.nested = [start]; 356 | } 357 | 358 | override void visit(const StructDeclaration dec) 359 | { 360 | handleType(TypeDetails.Type.struct_, dec.name.text, dec.name.index, dec); 361 | } 362 | 363 | override void visit(const UnionDeclaration dec) 364 | { 365 | handleType(TypeDetails.Type.union_, dec.name.text, dec.name.index, dec); 366 | } 367 | 368 | override void visit(const EnumDeclaration dec) 369 | { 370 | handleType(TypeDetails.Type.enum_, dec.name.text, dec.name.index, dec); 371 | } 372 | 373 | override void visit(const ClassDeclaration dec) 374 | { 375 | handleType(TypeDetails.Type.class_, dec.name.text, dec.name.index, dec); 376 | } 377 | 378 | override void visit(const InterfaceDeclaration dec) 379 | { 380 | handleType(TypeDetails.Type.interface_, dec.name.text, dec.name.index, dec); 381 | } 382 | 383 | override void visit(const TemplateDeclaration dec) 384 | { 385 | handleType(TypeDetails.Type.template_, dec.name.text, dec.name.index, dec); 386 | } 387 | 388 | override void visit(const AliasDeclaration dec) 389 | { 390 | if (dec && dec.declaratorIdentifierList) 391 | foreach (ident; dec.declaratorIdentifierList.identifiers) 392 | details.types ~= TypeDetails(nested ~ ident.text, ident.index, TypeDetails.Type.alias_); 393 | } 394 | 395 | void handleType(T)(TypeDetails.Type type, string name, size_t location, T node) 396 | { 397 | pushNestedType(type, name, location); 398 | super.visit(node); 399 | popNestedType(); 400 | } 401 | 402 | override void visit(const FunctionBody) 403 | { 404 | } 405 | 406 | alias visit = ASTVisitor.visit; 407 | 408 | protected void pushNestedType(TypeDetails.Type type, string name, size_t index) 409 | { 410 | nested ~= name; 411 | details.types ~= TypeDetails(nested, index, type); 412 | } 413 | 414 | protected void popNestedType() 415 | { 416 | nested.length--; 417 | } 418 | 419 | string[] nested; 420 | InterfaceDetails* details; 421 | } 422 | 423 | void formatTypeTransforming(Sink, T)(Sink sink, T node, string delegate(const(char)[]) translateType, 424 | bool useTabs = false, IndentStyle style = IndentStyle.allman, uint indentWith = 4) 425 | { 426 | TypeTransformingFormatter!Sink formatter = new TypeTransformingFormatter!(Sink)(sink, 427 | useTabs, style, indentWith); 428 | formatter.translateType = translateType; 429 | formatter.format(node); 430 | } 431 | 432 | /// 433 | class TypeTransformingFormatter(Sink) : Formatter!Sink 434 | { 435 | string delegate(const(char)[]) translateType; 436 | Appender!(char[]) tempBuffer; 437 | bool useTempBuffer; 438 | 439 | this(Sink sink, bool useTabs = false, IndentStyle style = IndentStyle.allman, uint indentWidth = 4) 440 | { 441 | super(sink, useTabs, style, indentWidth); 442 | tempBuffer = appender!(char[]); 443 | } 444 | 445 | override void put(string s) 446 | { 447 | if (useTempBuffer) 448 | tempBuffer.put(s); 449 | else 450 | super.put(s); 451 | } 452 | 453 | protected void flushTempBuffer() 454 | { 455 | if (!useTempBuffer || tempBuffer.data.empty) 456 | return; 457 | 458 | useTempBuffer = false; 459 | put(translateType(tempBuffer.data)); 460 | tempBuffer.clear(); 461 | } 462 | 463 | override void format(const TypeIdentifierPart type) 464 | { 465 | useTempBuffer = true; 466 | 467 | if (type.dot) 468 | { 469 | put("."); 470 | } 471 | if (type.identifierOrTemplateInstance) 472 | { 473 | format(type.identifierOrTemplateInstance); 474 | } 475 | if (type.indexer) 476 | { 477 | flushTempBuffer(); 478 | put("["); 479 | format(type.indexer); 480 | put("]"); 481 | } 482 | if (type.typeIdentifierPart) 483 | { 484 | put("."); 485 | format(type.typeIdentifierPart); 486 | } 487 | else 488 | { 489 | flushTempBuffer(); 490 | } 491 | } 492 | 493 | override void format(const IdentifierOrTemplateInstance identifierOrTemplateInstance) 494 | { 495 | with (identifierOrTemplateInstance) 496 | { 497 | format(identifier); 498 | if (templateInstance) 499 | { 500 | flushTempBuffer(); 501 | format(templateInstance); 502 | } 503 | } 504 | } 505 | 506 | alias format = Formatter!Sink.format; 507 | } 508 | --------------------------------------------------------------------------------