├── .github └── FUNDING.yml ├── LICENSE ├── README.org ├── app └── formatter.pl ├── pack.pl ├── prolog ├── _lsp_path_add.pl ├── lsp_changes.pl ├── lsp_checking.pl ├── lsp_colours.pl ├── lsp_completion.pl ├── lsp_formatter.pl ├── lsp_formatter_parser.pl ├── lsp_highlights.pl ├── lsp_parser.pl ├── lsp_reading_source.pl ├── lsp_server.pl └── lsp_utils.pl ├── run_tests.sh ├── scripts └── make_release.pl ├── test ├── changes.plt ├── checking.plt ├── checking_input1.pl ├── format_input1.pl ├── format_output1.pl ├── formatter.plt ├── highlight_input1.pl ├── highlights.plt ├── server.plt ├── utils.plt ├── utils_input1.pl └── utils_input2.pl └── vscode ├── .gitignore ├── .vscode └── settings.json ├── README.md ├── extension.js ├── package-lock.json ├── package.json └── prolog.config.json /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: jamesnvc 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 2-Clause License 2 | 3 | Copyright (c) 2019, James Cash 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | 1. Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | 2. Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 17 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 18 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 19 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 20 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 21 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 22 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 23 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 24 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 25 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 26 | -------------------------------------------------------------------------------- /README.org: -------------------------------------------------------------------------------- 1 | * Prolog Language server 2 | 3 | Still a work-in-progress -- please open an issue if you have any issues or feature requests!. 4 | 5 | Currently supports: 6 | - diagnostics (singleton variables, syntax errors) via xref 7 | - find references/definitions 8 | - documentation of predicates on hover 9 | - auto-completion of predicates 10 | - code formatting (✨new!✨) 11 | - symbol highlighting (✨new!✨) 12 | - variable renaming (✨new!✨) 13 | 14 | The code formatter can also be run stand-alone. After installing the ~lsp_server~ pack, you can run ~swipl formatter ~ at the command line. 15 | 16 | Only tested with SWI-Prolog, as it heavily uses its introspection facilities to do its stuff. 17 | It should work with any relatively-recent version of SWI-Prolog, but for best results (for "find references" in particular), use a version with ~xref_called/5~ (8.1.5 or newer; past commit [[https://github.com/SWI-Prolog/swipl-devel/commit/303f6430de5c9d7e225d8eb6fb8bb8b59e7c5f8f][303f6430de5c]]). 18 | 19 | Installable as a pack with ~swipl pack install lsp_server~ or ~?- pack_install(lsp_server).~ from a repl. 20 | 21 | As of version 2.5.0, running the server over a socket is now supported by passing in the commandline arguments ~port ~ (instead of ~stdio~). 22 | 23 | If running on Windows directly (i.e. not via WSL) running the server via stdio will NOT work and you will need to run the server over a socket, so please consult that section of the documention for your editor.. 24 | 25 | * Emacs 26 | 27 | ** [[https://github.com/emacs-lsp/lsp-mode][lsp-mode]]: 28 | 29 | *** socket server 30 | 31 | #+begin_src emacs-lisp 32 | (lsp-register-client 33 | (make-lsp-client 34 | :new-connection 35 | (lsp-tcp-connection (lambda (port) (list "swipl" 36 | "-g" "use_module(library(lsp_server))." 37 | "-g" "lsp_server:main" 38 | "-t" "halt" 39 | "--" "port" port))) 40 | :major-modes '(prolog-mode) 41 | :priority 1 42 | :multi-root t 43 | :server-id 'prolog-ls)) 44 | #+end_src 45 | *** stdio server 46 | 47 | #+begin_src emacs-lisp 48 | (lsp-register-client 49 | (make-lsp-client 50 | :new-connection 51 | (lsp-stdio-connection (list "swipl" 52 | "-g" "use_module(library(lsp_server))." 53 | "-g" "lsp_server:main" 54 | "-t" "halt" 55 | "--" "stdio")) 56 | :major-modes '(prolog-mode) 57 | :priority 1 58 | :multi-root t 59 | :server-id 'prolog-ls)) 60 | #+end_src 61 | 62 | ** [[https://github.com/joaotavora/eglot][eglot]] 63 | *** socket server 64 | #+begin_src emacs-lisp 65 | (setopt eglot-server-programs (cons 66 | (cons 'prolog-mode 67 | (list "swipl" 68 | "-O" 69 | "-g" "use_module(library(lsp_server))." 70 | "-g" "lsp_server:main" 71 | "-t" "halt" 72 | "--" "port" :autoport)) 73 | eglot-server-programs)) 74 | #+end_src 75 | 76 | *** stdio server 77 | #+begin_src emacs-lisp 78 | (setopt eglot-server-programs (cons 79 | (cons 'prolog-mode 80 | (list "swipl" 81 | "-O" 82 | "-g" "use_module(library(lsp_server))." 83 | "-g" "lsp_server:main" 84 | "-t" "halt" 85 | "--" "stdio")) 86 | eglot-server-programs)) 87 | #+end_src 88 | 89 | * Vim/Neovim 90 | 91 | * Neovim 92 | ** [[https://github.com/neoclide/coc.nvim][CoC]] 93 | *** stdio server 94 | Put the following in ~coc-settings.json~ (which you can access by using the command ~:CocConfig~). 95 | 96 | #+begin_src json 97 | {"languageserver": { 98 | "prolog-lsp": { 99 | "command": "swipl", 100 | "args": ["-g", "use_module(library(lsp_server)).", 101 | "-g", "lsp_server:main", 102 | "-t", "halt", 103 | "--", "stdio" 104 | ], 105 | "filetypes": ["prolog"] 106 | }} 107 | } 108 | #+end_src 109 | *** socket server 110 | 111 | CoC does not support automatically starting a socket server. 112 | If you are on Windows and using CoC and hence need a socket server, you'll have to manually start the LSP process by running the following command: 113 | 114 | #+begin_src sh 115 | swipl -g 'use_module(library(lsp_server))' -g 'lsp_server:main' -t halt -- port 12345 116 | #+end_src 117 | 118 | Where "12345" is an arbitrary, free port number. 119 | 120 | Then, with that started, add the following to ~coc-settings.json~ (accessed via ~:CocConfig~). 121 | 122 | #+begin_src json 123 | {"languageserver": { 124 | "prolog-lsp": { 125 | "host": "127.0.0.1", 126 | "port": 12345, 127 | "filetypes": ["prolog"] 128 | }} 129 | } 130 | #+end_src 131 | 132 | Ensuring the port entered in the config is the same one used when starting the server process 133 | 134 | ** Native LSP (for Neovim >= 0.11) 135 | 136 | *** stdio server 137 | Put the following in ~$XDG_CONFIG_DIR/nvim/lsp/prolog.lua~: 138 | 139 | #+begin_src lua 140 | return { 141 | cmd = { 'swipl', 142 | '-g', 'use_module(library(lsp_server))', 143 | '-g', 'lsp_server:main', 144 | '-t', 'halt', 145 | '--', 'stdio' }, 146 | root_markers = { '.git', }, 147 | filetypes = { 'prolog' }, 148 | } 149 | #+end_src 150 | 151 | And add ~vim.lsp.enable({'prolog'})~ to ~$XDG_CONFIG_DIR/nvim/init.lua~. 152 | 153 | *** socket server 154 | Put the following in ~$XDG_CONFIG_DIR/nvim/lsp/prolog.lua~: 155 | 156 | #+begin_src lua 157 | local find_port = function() 158 | local uv = vim.uv 159 | local tcp = uv.new_tcp() 160 | tcp:bind("127.0.0.1", 0) 161 | local port = tcp:getsockname().port 162 | tcp:close_reset() 163 | return port 164 | end 165 | return { 166 | cmd = function(...) 167 | local server_port = find_port() 168 | vim.system({'swipl', 169 | '-g', 'use_module(library(lsp_server))', 170 | '-g', 'lsp_server:main', 171 | '-t', 'halt', 172 | '--', 'port', server_port}, 173 | {}, 174 | function(...) 175 | print("LSP PROCESS EXITED", ...) 176 | end) 177 | vim.uv.sleep(500) 178 | return vim.lsp.rpc.connect('127.0.0.1', server_port)(...) 179 | end, 180 | root_markers = { '.git', }, 181 | filetypes = { 'prolog' }, 182 | } 183 | #+end_src 184 | 185 | And add ~vim.lsp.enable({'prolog'})~ to ~$XDG_CONFIG_DIR/nvim/init.lua~. 186 | 187 | ** Native LSP (for Neovim >= 0.5 < 0.11) 188 | 189 | Install the [[https://github.com/neovim/nvim-lspconfig][neovim/nvim-lspconfig]] package 190 | 191 | Put the following in ~$XDG_CONFIG_DIR/nvim/lua/lspconfig/prolog_lsp.lua~: 192 | 193 | #+begin_src lua 194 | local configs = require 'lspconfig/configs' 195 | local util = require 'lspconfig/util' 196 | 197 | configs.prolog_lsp = { 198 | default_config = { 199 | cmd = {"swipl", 200 | "-g", "use_module(library(lsp_server)).", 201 | "-g", "lsp_server:main", 202 | "-t", "halt", 203 | "--", "stdio"}; 204 | filetypes = {"prolog"}; 205 | root_dir = util.root_pattern("pack.pl"); 206 | }; 207 | docs = { 208 | description = [[ 209 | https://github.com/jamesnvc/prolog_lsp 210 | 211 | Prolog Language Server 212 | ]]; 213 | } 214 | } 215 | -- vim:et ts=2 sw=2 216 | #+end_src 217 | 218 | Then add the following to ~init.vim~: 219 | 220 | #+begin_src viml 221 | lua << EOF 222 | require('lspconfig/prolog_lsp') 223 | require('lspconfig').prolog_lsp.setup{} 224 | EOF 225 | #+end_src 226 | 227 | * LazyVim 228 | 229 | ** stdio 230 | Create the following file in ~$XDG_CONFIG_DIR/nvim/lua/plugins/lsp.lua~ 231 | 232 | #+begin_src lua 233 | return { 234 | { 235 | "neovim/nvim-lspconfig", 236 | opts = { 237 | servers = { 238 | prolog = {}, 239 | }, 240 | setup = { 241 | prolog = function(_, opts) 242 | local lspconfig = require("lspconfig") 243 | local configs = require("lspconfig.configs") 244 | local util = require("lspconfig.util") 245 | 246 | local root_files = { ".git", "pack.pl" } 247 | 248 | if not configs.prolog then 249 | configs.prolog = { 250 | default_config = { 251 | cmd = { 252 | "swipl", 253 | "-g", 254 | "use_module(library(lsp_server)).", 255 | "-g", 256 | "lsp_server:main", 257 | "-t", 258 | "halt", 259 | "--", 260 | "stdio", 261 | }, 262 | filetypes = { "prolog" }, 263 | single_file_support = true, 264 | root_dir = util.root_pattern(unpack(root_files)), 265 | settings = {}, 266 | }, 267 | commands = {}, 268 | docs = { 269 | description = [[ 270 | Prolog LSP server 271 | ]], 272 | }, 273 | } 274 | end 275 | lspconfig.prolog.setup(opts) 276 | end, 277 | }, 278 | }, 279 | }, 280 | } 281 | #+end_src 282 | 283 | * VSCode 284 | 285 | Choose one from the list below: 286 | 287 | - download the latest ~.vsix~ file from the [[https://github.com/jamesnvc/lsp_server/releases][releases page]] 288 | - clone this repo and copy/symlink the ~vscode/~ directory to ~~/.vscode/extensions/~ 289 | - clone and build the ~.vsix~ file yourself by the follwing steps: 290 | 1. ~cd /path/to/clone/vscode~ 291 | 2. ~npm install~ 292 | 3. ~npx vsce package~ 293 | 4. add the resulting ~.vsix~ to VSCode by clicking the ~...~ at the top right of the "Extensions" panel then selecting ~Install from VSIX...~ 294 | 295 | 296 | * Helix 297 | 298 | Helix already includes configuration for this Prolog LSP server, so it should mostly Just Work. 299 | 300 | However, the default configuration gives the '.pl' extension to perl, so to avoid having to manually do ~:set-language prolog~ each time, you can add the following to ~$XDG_CONFIG/helix/languages.toml~ to remove Perl's association with that extension: 301 | 302 | #+begin_src toml 303 | [[language]] 304 | name = "perl" 305 | file-types = ["perl"] 306 | #+end_src 307 | -------------------------------------------------------------------------------- /app/formatter.pl: -------------------------------------------------------------------------------- 1 | :- use_module(library(main)). 2 | 3 | :- include('../prolog/_lsp_path_add.pl'). 4 | 5 | :- use_module(lsp(lsp_formatter), [ file_formatted/2 ]). 6 | :- use_module(lsp(lsp_formatter_parser), [ emit_reified/2 ]). 7 | 8 | :- initialization(main, main). 9 | 10 | main(Argv) :- 11 | argv_options(Argv, Files, Options), 12 | check_options(Options), 13 | main(Files, Options). 14 | 15 | check_options(Opts) :- 16 | memberchk(in_place(true), Opts), 17 | memberchk(output_to(_), Opts), !, 18 | format(user_error, "If inplace is true, output-to doesn't make sense~n", []), 19 | halt(1). 20 | check_options(_). 21 | 22 | 23 | opt_type(inplace, in_place, boolean(true)). 24 | opt_type(output_to, output_to, file). 25 | 26 | opt_help(help(header), 27 | ["Reformat Prolog files.~n", 28 | "If ", ansi(bold, "~w", ["inplace"]), 29 | " is false and more than one file is specified, ", 30 | ansi(bold, "~w", ["output-to"]), " is interpretted as a directory to output"]). 31 | opt_help(help(usage), " [option ...] file ..."). 32 | opt_help(inplace, "Re-format file in place"). 33 | opt_help(output_to, "If IN_PLACE is false, file to output reformatted file to"). 34 | 35 | main([File], Options) :- 36 | \+ memberchk(in_place(false), Options), !, 37 | file_formatted(File, Formatted), 38 | setup_call_cleanup( 39 | open(File, write, S), 40 | emit_reified(S, Formatted), 41 | close(S) 42 | ). 43 | main([File], Options) :- 44 | memberchk(in_place(false), Options), !, 45 | ( memberchk(output_to(Output), Options) 46 | -> true ; Output = '-' ), 47 | file_formatted(File, Formatted), 48 | ( Output = '-' 49 | -> emit_reified(user_output, Formatted) 50 | ; setup_call_cleanup( 51 | open(Output, write, S), 52 | emit_reified(S, Formatted), 53 | close(S) 54 | ) 55 | ). 56 | -------------------------------------------------------------------------------- /pack.pl: -------------------------------------------------------------------------------- 1 | name(lsp_server). 2 | title('A Prolog LSP Server'). 3 | version('3.11.6'). 4 | author('James N. V. Cash', 'james.cash@occasionallycogent.com'). 5 | home('https://github.com/jamesnvc/lsp_server'). 6 | download('https://github.com/jamesnvc/lsp_server/releases/*.zip'). 7 | provides(lsp_server). 8 | -------------------------------------------------------------------------------- /prolog/_lsp_path_add.pl: -------------------------------------------------------------------------------- 1 | :- dynamic user:file_search_path/2. 2 | :- multifile user:file_search_path/2. 3 | 4 | :- prolog_load_context(directory, Dir), 5 | ( user:file_search_path(lsp_project, _) 6 | -> true 7 | ; asserta(user:file_search_path(lsp, Dir)) ). 8 | -------------------------------------------------------------------------------- /prolog/lsp_changes.pl: -------------------------------------------------------------------------------- 1 | :- module(lsp_changes, [handle_doc_changes/2, 2 | doc_text_fallback/2, 3 | doc_text/2]). 4 | /** LSP changes 5 | 6 | Module for tracking edits to the source, in order to be able to act on 7 | the code as it is in the editor buffer, before saving. 8 | 9 | @author James Cash 10 | */ 11 | 12 | :- use_module(library(readutil), [read_file_to_codes/3]). 13 | 14 | :- dynamic doc_text/2. 15 | 16 | %! handle_doc_changes(+File:atom, +Changes:list) is det. 17 | % 18 | % Track =Changes= to the file =File=. 19 | handle_doc_changes(_, []) :- !. 20 | handle_doc_changes(Path, [Change|Changes]) :- 21 | handle_doc_change(Path, Change), 22 | handle_doc_changes(Path, Changes). 23 | 24 | handle_doc_change(Path, Change) :- 25 | _{range: _{start: _{line: StartLine, character: StartChar}, 26 | end: _{line: _EndLine0, character: _EndChar}}, 27 | rangeLength: ReplaceLen, text: Text} :< Change, 28 | !, 29 | atom_codes(Text, ChangeCodes), 30 | doc_text_fallback(Path, OrigCodes), 31 | replace_codes(OrigCodes, StartLine, StartChar, ReplaceLen, ChangeCodes, 32 | NewText), 33 | retractall(doc_text(Path, _)), 34 | assertz(doc_text(Path, NewText)). 35 | handle_doc_change(Path, Change) :- 36 | _{range: _{start: _{line: StartLine, character: StartChar}, 37 | end: _{line: EndLine, character: EndChar}}, 38 | text: Text} :< Change, 39 | !, 40 | atom_codes(Text, ChangeCodes), 41 | doc_text_fallback(Path, OrigCodes), 42 | replace_codes_range(OrigCodes, StartLine, StartChar, EndLine, EndChar, ChangeCodes, 43 | NewText), 44 | retractall(doc_text(Path, _)), 45 | assertz(doc_text(Path, NewText)). 46 | handle_doc_change(Path, Change) :- 47 | retractall(doc_text(Path, _)), 48 | atom_codes(Change.text, TextCodes), 49 | assertz(doc_text(Path, TextCodes)). 50 | 51 | %! doc_text_fallback(+Path:atom, -Text:text) is det. 52 | % 53 | % Get the contents of the file at =Path=, either with the edits we've 54 | % been tracking in memory, or from the file on disc if no edits have 55 | % occured. 56 | doc_text_fallback(Path, Text) :- 57 | doc_text(Path, Text), !. 58 | doc_text_fallback(Path, Text) :- 59 | read_file_to_codes(Path, Text, []), 60 | assertz(doc_text(Path, Text)). 61 | 62 | %! replace_codes_range(Text, StartLine, StartChar, EndLine, EndChar, ReplaceText, -NewText) is det. 63 | replace_codes_range(Text, StartLine, StartChar, EndLine, EndChar, ReplaceText, NewText) :- 64 | phrase(replace_range(start, 0, 0, StartLine, StartChar, EndLine, EndChar, ReplaceText), 65 | Text, 66 | NewText). 67 | 68 | replace_range(start, StartLine, StartChar, StartLine, StartChar, EndLine, EndChar, NewText), NewText --> 69 | !, 70 | replace_range(finish, StartLine, StartChar, StartLine, StartChar, EndLine, EndChar, NewText). 71 | replace_range(finish, EndLine, EndChar, _, _, EndLine, EndChar, _) --> !, []. 72 | replace_range(start, StartLine, 0, StartLine, StartChar, EndLine, EndChar, NewText), Take --> 73 | { length(Take, StartChar) }, 74 | Take, !, 75 | replace_range(start, StartLine, StartChar, StartLine, StartChar, EndLine, EndChar, NewText). 76 | replace_range(finish, EndLine, Char, StartLine, StartChar, EndLine, EndChar, NewText) --> 77 | !, { ToSkip is EndChar - Char }, 78 | skip(ToSkip), 79 | replace_range(finish, EndLine, EndChar, StartLine, StartChar, EndLine, EndChar, NewText). 80 | replace_range(start, Line0, _, StartLine, StartChar, EndLine, EndChar, NewText), Line --> 81 | line(Line), 82 | { succ(Line0, Line1) }, 83 | replace_range(start, Line1, 0, StartLine, StartChar, EndLine, EndChar, NewText). 84 | replace_range(finish, Line0, _, StartLine, StartChar, EndLine, EndChar, NewText) --> 85 | line(_), 86 | { succ(Line0, Line1) }, 87 | replace_range(finish, Line1, 0, StartLine, StartChar, EndLine, EndChar, NewText). 88 | 89 | %! replace_codes(Text, StartLine, StartChar, ReplaceLen, ReplaceText, -NewText) is det. 90 | replace_codes(Text, StartLine, StartChar, ReplaceLen, ReplaceText, NewText) :- 91 | phrase(replace(StartLine, StartChar, ReplaceLen, ReplaceText), 92 | Text, 93 | NewText). 94 | 95 | replace(0, 0, 0, NewText), NewText --> !, []. 96 | replace(0, 0, Skip, NewText) --> 97 | !, skip(Skip), 98 | replace(0, 0, 0, NewText). 99 | replace(0, Chars, Skip, NewText), Take --> 100 | { length(Take, Chars) }, 101 | Take, !, 102 | replace(0, 0, Skip, NewText). 103 | replace(Lines1, Chars, Skip, NewText), Line --> 104 | line(Line), !, 105 | { succ(Lines0, Lines1) }, 106 | replace(Lines0, Chars, Skip, NewText). 107 | 108 | skip(0) --> !, []. 109 | skip(N) --> [_], { succ(N0, N) }, skip(N0). 110 | 111 | line([0'\n]) --> [0'\n], !. 112 | line([C|Cs]) --> [C], line(Cs). 113 | -------------------------------------------------------------------------------- /prolog/lsp_checking.pl: -------------------------------------------------------------------------------- 1 | :- module(lsp_checking, [check_errors/2]). 2 | /** LSP Checking 3 | 4 | Module for checking Prolog source files for errors and warnings. 5 | 6 | @author James Cash 7 | */ 8 | 9 | :- use_module(library(apply_macros)). 10 | :- use_module(library(assoc), [list_to_assoc/2, 11 | get_assoc/3]). 12 | :- use_module(library(apply), [maplist/3]). 13 | :- use_module(library(debug), [debug/3]). 14 | :- use_module(library(lists), [member/2]). 15 | :- use_module(library(prolog_xref), [xref_clean/1, xref_source/1]). 16 | 17 | :- include('_lsp_path_add.pl'). 18 | :- use_module(lsp(lsp_utils), [clause_variable_positions/3]). 19 | 20 | :- dynamic user:message_hook/3. 21 | :- multifile user:message_hook/3. 22 | 23 | %! check_errors(+Path:atom, -Errors:List) is det. 24 | % 25 | % =Errors= is a list of the errors in the file given by =Path=. 26 | % This predicate changes the =user:message_hook/3= hook. 27 | check_errors(Path, Errors) :- 28 | nb_setval(checking_errors, []), 29 | Hook = (user:message_hook(Term, Kind, Lines) :- 30 | prolog_load_context(term_position, Pos), 31 | stream_position_data(line_count, Pos, Line), 32 | stream_position_data(line_position, Pos, Char), 33 | nb_getval(checking_errors, ErrList), 34 | nb_setval(checking_errors, [e(Term, Kind, Lines, Line, Char)|ErrList]) 35 | ), 36 | setup_call_cleanup( 37 | assertz(Hook, Ref), 38 | ( xref_clean(Path), xref_source(Path) ), 39 | erase(Ref) 40 | ), 41 | nb_getval(checking_errors, ErrList), 42 | once(expand_errors(Path, ErrList, Errors-Errors)). 43 | 44 | expand_errors(Path, [e(singletons(_, SingletonVars), warning, _, ClauseLine, _)|InErrs], 45 | OutErrs-Tail0) :- !, 46 | clause_variable_positions(Path, ClauseLine, VariablePoses), 47 | list_to_assoc(VariablePoses, VarPoses), 48 | findall( 49 | NewErr, 50 | ( member(VarName, SingletonVars), 51 | atom_length(VarName, VarLen), 52 | get_assoc(VarName, VarPoses, [position(Line, Char)]), 53 | EndChar is Char + VarLen, 54 | format(string(Msg), "Singleton variable ~w", [VarName]), 55 | NewErr = _{severity: 2, 56 | source: "prolog_xref", 57 | range: _{start: _{line: Line, character: Char}, 58 | end: _{line: Line, character: EndChar}}, 59 | message: Msg} ), 60 | Tail0, 61 | Tail1 62 | ), 63 | expand_errors(Path, InErrs, OutErrs-Tail1). 64 | expand_errors(Path, [e(_, silent, _, _, _)|InErr], OutErrs-Tail) :- !, 65 | expand_errors(Path, InErr, OutErrs-Tail). 66 | expand_errors(Path, [e(_Term, error, Lines, _, _)|InErrs], OutErrs-[Err|Tail]) :- 67 | Lines = [url(_File:Line1:Col1), _, _, Msg0], !, 68 | ( Msg0 = Fmt-Params 69 | -> format(string(Msg), Fmt, Params) 70 | ; text_to_string(Msg0, Msg) ), 71 | succ(Line0, Line1), ( succ(Col0, Col1) ; Col0 = 0 ), 72 | Err = _{severity: 1, 73 | source: "prolog_xref", 74 | range: _{start: _{line: Line0, character: Col0}, 75 | end: _{line: Line1, character: 0}}, 76 | message: Msg 77 | }, 78 | expand_errors(Path, InErrs, OutErrs-Tail). 79 | expand_errors(Path, [e(_Term, Kind, Lines, _, _)|InErr], OutErrs-[Err|Tail]) :- 80 | kind_level(Kind, Level), 81 | Lines = ['~w:~d:~d: '-[Path, Line1, Char1]|Msgs0], !, 82 | maplist(expand_error_message, Msgs0, Msgs), 83 | atomic_list_concat(Msgs, Msg), 84 | succ(Line0, Line1), 85 | ( succ(Char0, Char1) ; Char0 = 0 ), 86 | Err = _{severity: Level, 87 | source: "prolog_xref", 88 | range: _{start: _{line: Line0, character: Char0}, 89 | end: _{line: Line1, character: 0}}, 90 | message: Msg 91 | }, 92 | expand_errors(Path, InErr, OutErrs-Tail). 93 | expand_errors(Path, [_Msg|InErr], OutErrs-Tail) :- !, 94 | expand_errors(Path, InErr, OutErrs-Tail). 95 | expand_errors(_, [], _-[]) :- !. 96 | 97 | expand_error_message(Format-Args, Formatted) :- 98 | !, format(string(Formatted), Format, Args). 99 | expand_error_message(Msg, Msg). 100 | 101 | kind_level(error, 1). 102 | kind_level(warning, 2). 103 | -------------------------------------------------------------------------------- /prolog/lsp_colours.pl: -------------------------------------------------------------------------------- 1 | :- module(lsp_colours, [file_colours/2, 2 | file_range_colours/4, 3 | token_types/1, 4 | token_modifiers/1]). 5 | /** LSP Colours 6 | 7 | Module with predicates for colourizing Prolog code, via library(prolog_colour). 8 | 9 | @author James Cash 10 | */ 11 | 12 | :- use_module(library(apply), [maplist/4]). 13 | :- use_module(library(apply_macros)). 14 | :- use_module(library(debug), [debug/3]). 15 | :- use_module(library(lists), [numlist/3, nth0/3]). 16 | :- use_module(library(prolog_colour), [prolog_colourise_stream/3, 17 | prolog_colourise_term/4]). 18 | :- use_module(library(prolog_source), [read_source_term_at_location/3]). 19 | :- use_module(library(yall)). 20 | 21 | :- include('_lsp_path_add.pl'). 22 | :- use_module(lsp(lsp_changes), [doc_text/2]). 23 | :- use_module(lsp(lsp_utils), [seek_to_line/2, 24 | linechar_offset/3]). 25 | 26 | token_types([namespace, 27 | type, 28 | class, 29 | enum, 30 | interface, 31 | struct, 32 | typeParameter, 33 | parameter, 34 | variable, 35 | property, 36 | enumMember, 37 | event, 38 | function, 39 | member, 40 | macro, 41 | keyword, 42 | modifier, 43 | comment, 44 | string, 45 | number, 46 | regexp, 47 | operator 48 | ]). 49 | token_modifiers([declaration, 50 | definition, 51 | readonly, 52 | static, 53 | deprecated, 54 | abstract, 55 | async, 56 | modification, 57 | documentation, 58 | defaultLibrary 59 | ]). 60 | 61 | token_types_dict(Dict) :- 62 | token_types(Types), 63 | length(Types, Len), 64 | Len0 is Len - 1, 65 | numlist(0, Len0, Ns), 66 | maplist([Type, Idx, Type-Idx]>>true, Types, Ns, 67 | Pairs), 68 | dict_create(Dict, _, Pairs). 69 | 70 | %! file_colours(+File, -Colours) is det. 71 | % 72 | % True when =Colours= is a list of colour information 73 | % corresponding to the file =File=. 74 | file_colours(File, Tuples) :- 75 | setup_call_cleanup( 76 | message_queue_create(Queue), 77 | ( thread_create(file_colours_helper(Queue, File), ThreadId), 78 | await_messages(Queue, Colours0, Colours0) ), 79 | ( thread_join(ThreadId), 80 | message_queue_destroy(Queue) ) 81 | ), 82 | sort(2, @=<, Colours0, Colours), 83 | flatten_colour_terms(File, Colours, Tuples). 84 | 85 | %! file_range_colours(+File, +Start, +End, -Colours) is det. 86 | % 87 | % True when =Colours= is a list of colour information corresponding 88 | % to file =File= covering the terms between =Start= and =End=. Note 89 | % that it may go beyond either bound. 90 | file_range_colours(File, Start, End, Tuples) :- 91 | setup_call_cleanup( 92 | message_queue_create(Queue), 93 | ( thread_create(file_term_colours_helper(Queue, File, Start, End), 94 | ThreadId), 95 | await_messages(Queue, Colours0, Colours0) ), 96 | ( thread_join(ThreadId), 97 | message_queue_destroy(Queue) ) 98 | ), 99 | sort(2, @=<, Colours0, Colours), 100 | flatten_colour_terms(File, Colours, Tuples). 101 | 102 | file_stream(File, S) :- 103 | doc_text(File, Changes) 104 | -> open_string(Changes, S) 105 | ; open(File, read, S). 106 | 107 | %! flatten_colour_terms(+File, +ColourTerms, -Nums) is det. 108 | % 109 | % Convert the list of =ColourTerms= like =colour(Category, Start, 110 | % Length)= to a flat list of numbers in the format that LSP expects. 111 | % 112 | % @see https://microsoft.github.io/language-server-protocol/specifications/specification-3-16/#textDocument_semanticTokens 113 | flatten_colour_terms(File, ColourTerms, Nums) :- 114 | token_types_dict(TokenDict), 115 | setup_call_cleanup( 116 | file_stream(File, S), 117 | ( set_stream_position(S, '$stream_position'(0,0,0,0)), 118 | colour_terms_to_tuples(ColourTerms, Nums-Nums, 119 | S, TokenDict, 120 | 0, 0, 0) ), 121 | close(S) 122 | ). 123 | 124 | colour_terms_to_tuples([], _-[], 125 | _Stream, _Dict, 126 | _Offset, _Line, _Char). 127 | colour_terms_to_tuples([Colour|Colours], Tuples-T0, 128 | Stream, Dict, 129 | LastOffset, LastLine, LastChar) :- 130 | colour_term_to_tuple(Stream, Dict, 131 | LastOffset, LastLine, LastChar, 132 | ThisOffset, ThisLine, ThisChar, 133 | Colour, 134 | T0-T1), !, 135 | colour_terms_to_tuples(Colours, Tuples-T1, 136 | Stream, Dict, 137 | ThisOffset, ThisLine, ThisChar). 138 | colour_terms_to_tuples([colour(_Type, _, _)|Colours], Tuples, 139 | Stream, Dict, 140 | ThisOffset, ThisLine, ThisChar) :- 141 | % ( memberchk(Type, [clause, body, list, empty_list, brace_term, parentheses, 142 | % range, goal(_, _), head(_, _), dict, dict_content, 143 | % term, error]) 144 | % -> true 145 | % ; debug(server, "Unhighlighted term ~w", [Type]) 146 | % ), 147 | colour_terms_to_tuples(Colours, Tuples, 148 | Stream, Dict, 149 | ThisOffset, ThisLine, ThisChar). 150 | 151 | colour_term_to_tuple(Stream, Dict, 152 | LastOffset, LastLine, LastChar, 153 | Offset, Line, Char, 154 | colour(Type, Offset, Len), 155 | [DeltaLine, DeltaStart, Len, TypeCode, ModMask|T1]-T1) :- 156 | colour_type(Type, TypeCategory, Mods), 157 | get_dict(TypeCategory, Dict, TypeCode), 158 | mods_mask(Mods, ModMask), !, 159 | Seek is Offset - LastOffset, 160 | setup_call_cleanup(open_null_stream(NullStream), 161 | copy_stream_data(Stream, NullStream, Seek), 162 | close(NullStream)), 163 | stream_property(Stream, position(Pos)), 164 | stream_position_data(line_count, Pos, Line), 165 | stream_position_data(line_position, Pos, Char), 166 | ( Line == LastLine 167 | -> ( DeltaLine = 0, 168 | DeltaStart is Char - LastChar 169 | ) 170 | ; ( DeltaLine is Line - LastLine, 171 | DeltaStart = Char 172 | ) 173 | ). 174 | 175 | colour_type(directive, namespace, []). 176 | colour_type(head_term(_, _), function, [declaration]). 177 | colour_type(neck(directive), operator, [declaration]). 178 | colour_type(neck(clause), operator, [definition]). 179 | colour_type(neck(grammar_rule), operator, [definition]). 180 | colour_type(goal_term(built_in, A), macro, []) :- atom(A), !. 181 | colour_type(goal_term(built_in, _), function, [defaultLibrary]). 182 | colour_type(goal_term(undefined, _), function, []). 183 | colour_type(goal_term(imported(_), _), function, []). 184 | colour_type(goal_term(local(_), _), function, []). 185 | colour_type(goal_term(extern(_,_), _), function, []). 186 | colour_type(goal_term(recursion, _), member, []). 187 | colour_type(goal_term(('dynamic'(_)), _), parameter, []). 188 | colour_type(atom, string, []). 189 | colour_type(var, variable, []). 190 | colour_type(singleton, variable, [readonly]). 191 | colour_type(fullstop, operator, []). 192 | colour_type(control, operator, []). 193 | colour_type(dict_key, property, []). 194 | colour_type(dict_sep, operator, []). 195 | colour_type(string, string, []). 196 | colour_type(int, number, []). 197 | colour_type(comment(line), comment, []). 198 | colour_type(comment(structured), comment, [documentation]). 199 | colour_type(arity, parameter, []). 200 | colour_type(functor, struct, []). 201 | colour_type(option_name, struct, []). 202 | colour_type(predicate_indicator, interface, []). 203 | colour_type(predicate_indicator(_, _), interface, []). 204 | colour_type(unused_import, macro, [deprecated]). 205 | colour_type(undefined_import, macro, [deprecated]). 206 | colour_type(dcg, regexp, []). 207 | colour_type(dcg(terminal), regexp, []). 208 | colour_type(dcg(plain), function, []). 209 | colour_type(dcg_right_hand_ctx, regexp, []). 210 | colour_type(grammar_rule, regexp, []). 211 | colour_type(identifier, namespace, []). 212 | colour_type(file(_), namespace, []). 213 | colour_type(file_no_depend(_), namespace, [abstract]). 214 | colour_type(module(_), namespace, []). 215 | 216 | mods_mask(Mods, Mask) :- 217 | mods_mask(Mods, 0, Mask). 218 | 219 | mods_mask([], Mask, Mask). 220 | mods_mask([Mod|Mods], Mask0, Mask) :- 221 | token_modifiers(ModsList), 222 | nth0(N, ModsList, Mod), 223 | Mask1 is Mask0 \/ (1 << N), 224 | mods_mask(Mods, Mask1, Mask). 225 | 226 | %%% Helpers 227 | 228 | %! await_messages(+Queue, ?Head, -Tail) is det. 229 | % 230 | % Helper predicate to accumulate messages from 231 | % =file_colours_helper/2= in a list. 232 | await_messages(Q, H, T) :- 233 | thread_get_message(Q, Term), 234 | ( Term == done 235 | -> T = [] 236 | ; ( T = [Term|T0], 237 | await_messages(Q, H, T0) 238 | ) 239 | ). 240 | 241 | %! file_colours_helper(+Queue, +File) is det. 242 | % 243 | % Use =prolog_colourise_stream/3= to accumulate a list of colour 244 | % terms. Does it in this weird way sending messages to a queue 245 | % because the predicate takes a closure but we want to get a list of 246 | % all of the terms. 247 | file_colours_helper(Queue, File) :- 248 | setup_call_cleanup( 249 | file_stream(File, S), 250 | prolog_colourise_stream( 251 | S, File, 252 | {Queue}/[Cat, Start, Len]>>( 253 | thread_send_message(Queue, colour(Cat, Start, Len))) 254 | ), 255 | close(S) 256 | ), 257 | thread_send_message(Queue, done). 258 | 259 | nearest_term_start(Stream, StartL, TermStart) :- 260 | read_source_term_at_location(Stream, _, [line(StartL), error(Error)]), 261 | ( nonvar(Error) 262 | -> ( LineBack is StartL - 1, 263 | nearest_term_start(Stream, LineBack, TermStart) ) 264 | ; TermStart = StartL 265 | ). 266 | 267 | file_term_colours_helper(Queue, File, 268 | line_char(StartL, _StartC), 269 | End) :- 270 | setup_call_cleanup( 271 | file_stream(File, S), 272 | ( nearest_term_start(S, StartL, TermLine), 273 | seek(S, 0, bof, _), 274 | set_stream_position(S, '$stream_position'(0,0,0,0)), 275 | seek_to_line(S, TermLine), 276 | colourise_terms_to_position(Queue, File, S, 0-0, End) 277 | ), 278 | close(S) 279 | ), 280 | thread_send_message(Queue, done). 281 | 282 | colourise_terms_to_position(Queue, File, Stream, Prev, End) :- 283 | prolog_colourise_term( 284 | Stream, File, 285 | {Queue}/[Cat, Start, Len]>>( 286 | thread_send_message(Queue, colour(Cat, Start, Len))), 287 | []), 288 | stream_property(Stream, position(Pos)), 289 | stream_position_data(line_count, Pos, Line), 290 | stream_position_data(line_position, Pos, Char), 291 | End = line_char(EndL, EndC), 292 | ( Line-Char == Prev 293 | -> true 294 | ; EndL =< Line 295 | -> true 296 | ; ( EndL == Line, EndC =< Char ) 297 | -> true 298 | ; colourise_terms_to_position(Queue, File, Stream, Line-Char, End) 299 | ). 300 | -------------------------------------------------------------------------------- /prolog/lsp_completion.pl: -------------------------------------------------------------------------------- 1 | :- module(lsp_completion, [completions_at/3]). 2 | /** LSP Completion 3 | 4 | This module implements code completion, based on defined predicates in 5 | the file & imports. 6 | 7 | Uses =lsp_changes= in order to see the state of the buffer being edited. 8 | 9 | @see lsp_changes:doc_text_fallback/2 10 | 11 | @author James Cash 12 | */ 13 | 14 | :- use_module(library(apply), [ maplist/3 ]). 15 | :- use_module(library(lists), [ numlist/3 ]). 16 | :- use_module(library(prolog_xref), [ xref_defined/3, xref_source/2 ]). 17 | :- use_module(library(yall)). 18 | 19 | :- include('_lsp_path_add.pl'). 20 | 21 | :- use_module(lsp(lsp_utils), [ linechar_offset/3 ]). 22 | :- use_module(lsp(lsp_changes), [ doc_text_fallback/2 ]). 23 | 24 | part_of_prefix(Code) :- code_type(Code, prolog_var_start). 25 | part_of_prefix(Code) :- code_type(Code, prolog_atom_start). 26 | part_of_prefix(Code) :- code_type(Code, prolog_identifier_continue). 27 | 28 | get_prefix_codes(Stream, Offset, Codes) :- 29 | get_prefix_codes(Stream, Offset, [], Codes). 30 | 31 | get_prefix_codes(Stream, Offset0, Codes0, Codes) :- 32 | peek_code(Stream, Code), 33 | part_of_prefix(Code), !, 34 | succ(Offset1, Offset0), 35 | seek(Stream, Offset1, bof, Offset), 36 | get_prefix_codes(Stream, Offset, [Code|Codes0], Codes). 37 | get_prefix_codes(_, _, Codes, Codes). 38 | 39 | prefix_at(File, Position, Prefix) :- 40 | doc_text_fallback(File, DocCodes), 41 | setup_call_cleanup( 42 | open_string(DocCodes, Stream), 43 | ( linechar_offset(Stream, Position, _), 44 | seek(Stream, -1, current, Offset), 45 | get_prefix_codes(Stream, Offset, PrefixCodes), 46 | string_codes(Prefix, PrefixCodes) ), 47 | close(Stream) 48 | ). 49 | 50 | completions_at(File, Position, Completions) :- 51 | prefix_at(File, Position, Prefix), 52 | xref_source(File, [silent(true)]), 53 | findall( 54 | Result, 55 | ( xref_defined(File, Goal, _), 56 | functor(Goal, Name, Arity), 57 | atom_concat(Prefix, _, Name), 58 | ( predicate_arguments(File, Name/Arity, Args) -> true ; args_str(Arity, Args) ), 59 | format(string(Func), "~w(~w)$0", [Name, Args]), 60 | format(string(Label), "~w/~w", [Name, Arity]), 61 | Result = _{label: Label, 62 | insertText: Func, 63 | insertTextFormat: 2} ), 64 | Completions, 65 | CompletionsTail 66 | ), 67 | findall( 68 | Result, 69 | ( predicate_property(system:Goal, built_in), 70 | functor(Goal, Name, Arity), 71 | atom_concat(Prefix, _, Name), 72 | \+ sub_atom(Name, 0, _, _, '$'), 73 | ( predicate_arguments(File, Name/Arity, Args) -> true ; args_str(Arity, Args) ), 74 | format(string(Func), "~w(~w)$0", [Name, Args]), 75 | format(string(Label), "~w/~w", [Name, Arity]), 76 | Result = _{label: Label, 77 | insertText: Func, 78 | insertTextFormat: 2} ), 79 | CompletionsTail 80 | ). 81 | 82 | predicate_arguments(File, Pred/Arity, ArgsStr) :- 83 | lsp_utils:predicate_help(File, Pred/Arity, HelpText), 84 | string_concat(Pred, "(", PredName), 85 | sub_string(HelpText, BeforeName, NameLen, _, PredName), 86 | sub_string(HelpText, BeforeClose, _, _, ")"), 87 | BeforeClose > BeforeName, !, 88 | ArgsStart is BeforeName + NameLen, 89 | ArgsLength is BeforeClose - ArgsStart, 90 | sub_string(HelpText, ArgsStart, ArgsLength, _, ArgsStr0), 91 | atomic_list_concat(Args, ', ', ArgsStr0), 92 | length(Args, Length), 93 | Arity = Length, 94 | numlist(1, Length, Nums), 95 | maplist([Arg, Num, S]>>format(string(S), "${~w:~w}", [Num, Arg]), 96 | Args, Nums, Args1), 97 | atomic_list_concat(Args1, ', ', ArgsStr). 98 | 99 | args_str(Arity, Str) :- 100 | numlist(1, Arity, Args), 101 | maplist([A, S]>>format(string(S), "${~w:_}", [A]), 102 | Args, ArgStrs), 103 | atomic_list_concat(ArgStrs, ', ', Str). 104 | -------------------------------------------------------------------------------- /prolog/lsp_formatter.pl: -------------------------------------------------------------------------------- 1 | :- module(lsp_formatter, [ file_format_edits/2, 2 | file_formatted/2 ]). 3 | 4 | /** LSP Formatter 5 | 6 | Module for formatting Prolog source code 7 | 8 | @author James Cash 9 | 10 | */ 11 | 12 | :- use_module(library(readutil), [ read_file_to_string/3 ]). 13 | :- use_module(library(macros)). 14 | 15 | :- include('_lsp_path_add.pl'). 16 | :- use_module(lsp(lsp_formatter_parser), [ reified_format_for_file/2, 17 | emit_reified/2 ]). 18 | file_format_edits(Path, Edits) :- 19 | read_file_to_string(Path, OrigText, []), 20 | split_string(OrigText, "\n", "", OrigLines), 21 | file_formatted(Path, Formatted), 22 | with_output_to(string(FormattedText), 23 | emit_reified(current_output, Formatted)), 24 | split_string(FormattedText, "\n", "", FormattedLines), 25 | create_edit_list(OrigLines, FormattedLines, Edits). 26 | 27 | file_formatted(Path, Formatted) :- 28 | reified_format_for_file(Path, Reified), 29 | apply_format_rules(Reified, Formatted). 30 | 31 | %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% 32 | % Formatting rules 33 | %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% 34 | 35 | apply_format_rules(Content, Formatted) :- 36 | phrase(formatter_rules, Content, Formatted). 37 | 38 | formatter_rules --> 39 | collapse_whitespace, 40 | commas_exactly_one_space, 41 | correct_indentation(_{state: [toplevel], column: 0, leading_spaces: []}). 42 | 43 | collapse_whitespace([], []) :- !. 44 | collapse_whitespace([white(A), white(B)|InRest], [white(AB)|OutRest]) :- !, 45 | AB is A + B, 46 | collapse_whitespace(InRest, OutRest). 47 | collapse_whitespace([In|InRest], [In|OutRest]) :- 48 | collapse_whitespace(InRest, OutRest). 49 | 50 | commas_exactly_one_space([], Out) => Out = []. 51 | commas_exactly_one_space([white(_), comma|InRest], Out) => 52 | commas_exactly_one_space([comma|InRest], Out). 53 | commas_exactly_one_space([comma, white(_)|InRest], Out), InRest \= [comment(_)|_] => 54 | Out = [comma, white(1)|OutRest], 55 | commas_exactly_one_space(InRest, OutRest). 56 | commas_exactly_one_space([comma, Next|InRest], Out), Next \= white(_), Next \= newline => 57 | Out = [comma, white(1), Next|OutRest], 58 | commas_exactly_one_space(InRest, OutRest). 59 | commas_exactly_one_space([Other|Rest], Out) => 60 | Out = [Other|OutRest], 61 | commas_exactly_one_space(Rest, OutRest). 62 | 63 | #define(toplevel_indent, 4). 64 | 65 | correct_indentation(_, [], []) :- !. 66 | correct_indentation(State0, 67 | [term_begin(Func, Type, Parens)|InRest], 68 | [term_begin(Func, Type, Parens)|OutRest]) :- 69 | indent_state_top(State0, toplevel), 70 | Func = ':-', !, 71 | indent_state_push(State0, declaration, State1), 72 | update_state_column(State1, term_begin(Func, Type, Parens), State2), 73 | push_state_open_spaces(State2, InRest, State3), 74 | correct_indentation(State3, InRest, OutRest). 75 | correct_indentation(State0, 76 | [term_begin(Func, Type, Parens)|InRest], 77 | [term_begin(Func, Type, Parens)|OutRest]) :- 78 | indent_state_top(State0, toplevel), !, 79 | update_state_column(State0, term_begin(Func, Type, Parens), State1), 80 | indent_state_push(State1, defn_head(State1.column, false), State2), 81 | push_state_open_spaces(State2, InRest, State3), 82 | correct_indentation(State3, InRest, OutRest). 83 | correct_indentation(State0, [In|InRest], [In|OutRest]) :- 84 | indent_state_top(State0, toplevel), 85 | In = simple(_), !, 86 | indent_state_push(State0, defn_head_neck, State1), 87 | update_state_column(State1, In, State2), 88 | correct_indentation(State2, InRest, OutRest). 89 | correct_indentation(State0, 90 | [term_begin(Neckish, T, P)|InRest], 91 | [term_begin(Neckish, T, P)|OutRest]) :- 92 | memberchk(Neckish, [':-', '=>', '-->']), 93 | indent_state_top(State0, defn_head_neck), !, 94 | indent_state_pop(State0, State1), 95 | indent_state_push(State1, defn_body, State2), 96 | update_state_column(State2, term_begin(Neckish, T, P), State3), 97 | push_state_open_spaces(State3, InRest, State4), 98 | correct_indentation(State4, InRest, OutRest). 99 | correct_indentation(State0, [In|InRest], Out) :- 100 | once((In = term_begin('->', compound, false) 101 | ; In = term_begin(';', compound, false))), 102 | indent_state_top(State0, defn_body_indent), !, 103 | indent_state_pop(State0, State1), 104 | outdent_align(State1, Indent), 105 | Out = [white(Indent)|OutRest], 106 | update_state_column(State1, white(Indent), State4), 107 | correct_indentation(State4, [In|InRest], OutRest). 108 | correct_indentation(State0, [newline|InRest], [newline|Out]) :- !, 109 | ( indent_state_top(State0, defn_body_indent) 110 | -> State1 = State0 111 | ; indent_state_push(State0, defn_body_indent, State1) ), 112 | update_state_column(State1, newline, State2), 113 | correct_indentation(State2, InRest, Out). 114 | correct_indentation(State0, [In|InRest], Out) :- 115 | indent_state_top(State0, defn_body_indent), !, 116 | ( In = white(_) 117 | -> correct_indentation(State0, InRest, Out) 118 | ; insert_whitespace_to_indent(State0, [In|InRest], Out) ). 119 | correct_indentation(State0, [In|InRest], [In|OutRest]) :- 120 | In = term_begin(';', compound, false), !, 121 | update_alignment(State0, State1), 122 | update_state_column(State1, In, State2), 123 | %% indent_state_top(State2, StateTop), 124 | copy_current_alignment(State2, CurrentAlign), 125 | indent_state_push(State2, CurrentAlign, State3), 126 | push_state_open_spaces(State3, InRest, State4), 127 | correct_indentation(State4, InRest, OutRest). 128 | correct_indentation(State0, [In|InRest], [In|OutRest]) :- 129 | functor(In, Name, _Arity, _Type), 130 | atom_concat(_, '_begin', Name), !, 131 | % if we've just begun something... 132 | update_alignment(State0, State1), 133 | update_state_column(State1, In, State2), 134 | indent_state_push(State2, begin(State2.column, State1.column), State3), 135 | push_state_open_spaces(State3, InRest, State4), 136 | correct_indentation(State4, InRest, OutRest). 137 | correct_indentation(State0, [In|InRest], [In|OutRest]) :- 138 | indent_state_top(State0, defn_head(_, _)), 139 | In = term_end(_, S), S \= toplevel, !, 140 | indent_state_pop(State0, State1), 141 | indent_state_push(State1, defn_head_neck, State2), 142 | update_state_column(State2, In, State3), 143 | pop_state_open_spaces(State3, _, State4), 144 | correct_indentation(State4, InRest, OutRest). 145 | correct_indentation(State0, [In|InRest], Out) :- 146 | ending_term(In), !, 147 | indent_state_pop(State0, State1), 148 | update_state_column(State1, In, State2), 149 | pop_state_open_spaces(State2, Spaces, State3), 150 | ( In \= term_end(false, _), In \= term_end(_, toplevel), Spaces > 0 151 | -> Out = [white(Spaces), In|OutRest] 152 | ; Out = [In|OutRest] ), 153 | correct_indentation(State3, InRest, OutRest). 154 | correct_indentation(State0, [In, NextIn|InRest], Out) :- 155 | In = white(_), 156 | ending_term(NextIn), !, 157 | correct_indentation(State0, [NextIn|InRest], Out). 158 | correct_indentation(State0, [In|InRest], [In|OutRest]) :- 159 | memberchk(In, [white(_), newline]), !, 160 | update_state_column(State0, In, State1), 161 | correct_indentation(State1, InRest, OutRest). 162 | correct_indentation(State0, [In|InRest], [In|OutRest]) :- !, 163 | ( In \= white(_) 164 | -> update_alignment(State0, State1) 165 | ; State1 = State0 ), 166 | update_state_column(State1, In, State2), 167 | correct_indentation(State2, InRest, OutRest). 168 | 169 | copy_current_alignment(State, Alignment), indent_state_top(State, defn_body) => 170 | Alignment = align(#toplevel_indent, 4). 171 | copy_current_alignment(State, Alignment), indent_state_top(State, align(_, _)) => 172 | indent_state_top(State, Alignment). 173 | copy_current_alignment(State, Alignment), indent_state_top(State, begin(Col, BeganAt)) => 174 | Alignment = begin(Col, BeganAt). 175 | copy_current_alignment(State, Alignment), indent_state_top(State, defn_body_indent) => 176 | Alignment = align(#toplevel_indent, 4). 177 | copy_current_alignment(State, Alignment), indent_state_top(State, defn_head(Column, _Aligned)) => 178 | Alignment = align(Column, Column). 179 | copy_current_alignment(State, Alignment), indent_state_top(State, defn_head_neck) => 180 | Alignment = align(#toplevel_indent, 4). 181 | copy_current_alignment(State, Alignment), indent_state_top(State, declaration) => 182 | Alignment = align(2, 2). 183 | copy_current_alignment(State, Alignment) => 184 | indent_state_top(State, Alignment). 185 | 186 | insert_whitespace_to_indent(State0, [In|InRest], Out) :- 187 | indent_state_pop(State0, State1), 188 | ( indent_state_top(State1, begin(_, BeganAt)) 189 | % state top = begin means prev line ended with an open paren 190 | -> % so pop that off and align as if one step "back" 191 | indent_state_pop(State1, StateX), 192 | whitespace_indentation_for_state(StateX, PrevIndent), 193 | IncPrevIndent is PrevIndent + 4, 194 | indent_state_push(StateX, align(IncPrevIndent, BeganAt), State2) 195 | ; State2 = State1 ), 196 | update_alignment(State2, State3), 197 | ( ending_term(In) 198 | -> indent_for_end_term(State3, In, State4, Indent) 199 | ; whitespace_indentation_for_state(State3, Indent), 200 | State4 = State3 ), 201 | Out = [white(Indent)|OutRest], 202 | update_state_column(State4, white(Indent), State5), 203 | correct_indentation(State5, [In|InRest], OutRest). 204 | 205 | indent_for_end_term(State0, In, State, Indent) :- 206 | % for a paren ending a term, align a level up 207 | In = term_end(true, _), !, 208 | indent_state_pop(State0, State_), 209 | pop_state_open_spaces(State0, _, State1), 210 | push_state_open_spaces(State1, 0, State), 211 | whitespace_indentation_for_state(State_, Indent). 212 | indent_for_end_term(State0, In, State, Indent) :- 213 | % for a brace ending a dict, align two levels up level up 214 | In = dict_end, !, 215 | indent_state_pop(State0, State_), 216 | indent_state_pop(State_, State__), 217 | pop_state_open_spaces(State0, _, State1), 218 | push_state_open_spaces(State1, 0, State), 219 | whitespace_indentation_for_state(State__, Indent). 220 | indent_for_end_term(State0, _In, State, Indent) :- 221 | % for another ending term, align to the open 222 | % if we have alignment infomation. 223 | indent_state_top(State0, Top), 224 | Top = align(_, Indent), !, 225 | pop_state_open_spaces(State0, _, State1), 226 | push_state_open_spaces(State1, 0, State). 227 | indent_for_end_term(State0, _In, State, Indent) :- 228 | % otherwise, at top-level, just pop state. 229 | indent_state_pop(State0, State_), 230 | pop_state_open_spaces(State0, _, State1), 231 | push_state_open_spaces(State1, 0, State), 232 | whitespace_indentation_for_state(State_, Indent). 233 | 234 | ending_term(Term) :- 235 | functor(Term, Name, _, _), 236 | atom_concat(_, '_end', Name). 237 | 238 | outdent_align(State, Outdented) :- 239 | whitespace_indentation_for_state(State, Indent), 240 | Outdented is Indent - 2. 241 | 242 | update_alignment(State0, State2) :- 243 | indent_state_top(State0, begin(Col, BeganAt)), !, 244 | indent_state_pop(State0, State1), 245 | AlignCol is max(Col, State1.column), 246 | indent_state_push(State1, align(AlignCol, BeganAt), State2). 247 | update_alignment(State0, State2) :- 248 | indent_state_top(State0, defn_head(Col, false)), !, 249 | indent_state_pop(State0, State1), 250 | AlignCol is max(Col, State1.column), 251 | indent_state_push(State1, defn_head(AlignCol, true), State2). 252 | update_alignment(State, State). 253 | 254 | whitespace_indentation_for_state(State, Indent) :- 255 | indent_state_top(State, align(Indent, _)), !. 256 | whitespace_indentation_for_state(State, Indent) :- 257 | indent_state_top(State, defn_head(Indent, _)), !. 258 | whitespace_indentation_for_state(State, Indent) :- 259 | get_dict(state, State, Stack), 260 | aggregate_all(count, 261 | ( member(X, Stack), 262 | memberchk(X, [parens_begin, braces_begin, term_begin(_, _, _)]) ), 263 | ParensCount), 264 | ( indent_state_contains(State, defn_body) 265 | -> MoreIndent = #toplevel_indent 266 | ; MoreIndent = 0 ), 267 | Indent is ParensCount * 2 + MoreIndent. 268 | 269 | indent_state_top(State, Top) :- 270 | _{state: [Top|_]} :< State. 271 | 272 | indent_state_contains(State, Needle) :- 273 | _{state: Stack} :< State, 274 | memberchk(Needle, Stack). 275 | 276 | indent_state_push(State0, NewTop, State1) :- 277 | _{state: Stack} :< State0, 278 | put_dict(state, State0, [NewTop|Stack], State1). 279 | 280 | indent_state_pop(State0, State1) :- 281 | _{state: [_|Rest]} :< State0, 282 | put_dict(state, State0, Rest, State1). 283 | 284 | update_state_column(State0, newline, State1) :- !, 285 | put_dict(column, State0, 0, State1). 286 | update_state_column(State0, Term, State1) :- 287 | emit_reified(string(S), [Term]), 288 | string_length(S, Len), 289 | NewCol is State0.column + Len, 290 | put_dict(column, State0, NewCol, State1). 291 | 292 | push_state_open_spaces(State0, Next, State1) :- 293 | _{leading_spaces: PrevSpaces} :< State0, 294 | ( Next = [white(N)|_] 295 | -> put_dict(leading_spaces, State0, [N|PrevSpaces], State1) 296 | ; put_dict(leading_spaces, State0, [0|PrevSpaces], State1) ). 297 | 298 | pop_state_open_spaces(State0, Top, State1) :- 299 | _{leading_spaces: [Top|Spaces]} :< State0, 300 | put_dict(leading_spaces, State0, Spaces, State1). 301 | 302 | %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% 303 | % Create a List of Edits from the Original and Formatted Lines 304 | %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% 305 | create_edit_list(Orig, Formatted, Edits) :- 306 | create_edit_list(0, Orig, Formatted, Edits). 307 | 308 | create_edit_list(_, [], [], []) :- !. 309 | create_edit_list(LineNum, [Line|Lines], [], [Edit]) :- !, 310 | length(Lines, NLines), 311 | EndLine is LineNum + NLines, 312 | last([Line|Lines], LastLine), 313 | string_length(LastLine, LastLineLen), 314 | Edit = _{range: _{start: _{line: LineNum, character: 0}, 315 | end: _{line: EndLine, character: LastLineLen}}, 316 | newText: ""}. 317 | create_edit_list(LineNum, [], [NewLine|NewLines], [Edit|Edits]) :- !, 318 | string_length(NewLine, LenLen), 319 | Edit = _{range: _{start: _{line: LineNum, character: 0}, 320 | end: _{line: LineNum, character: LenLen}}, 321 | newText: NewLine}, 322 | succ(LineNum, LineNum1), 323 | create_edit_list(LineNum1, [], NewLines, Edits). 324 | create_edit_list(LineNum, [OrigLine|OrigRest], [FormattedLine|FormattedRest], Edits) :- 325 | ( OrigLine \= FormattedLine % Only create an edit if the line has changed 326 | -> string_length(OrigLine, LineLen), %TODO: what should this be? 327 | Edit = _{range: _{start: _{line: LineNum, character: 0}, 328 | end: _{line: LineNum, character: LineLen}}, 329 | newText: FormattedLine}, 330 | Edits = [Edit|EditRest] 331 | ; EditRest = Edits 332 | ), 333 | succ(LineNum, LineNum1), 334 | create_edit_list(LineNum1, OrigRest, FormattedRest, EditRest). 335 | 336 | % lsp_formatter:file_formatted('/Users/james/Projects/prolog-lsp/prolog/format_test2.pl', Src), lsp_formatter_parser:emit_reified(user_output, Src). 337 | 338 | % lsp_formatter:file_formatted('/Users/james/Projects/prolog-lsp/prolog/format_test.pl', Src), setup_call_cleanup(open('/Users/james/tmp/formatted_out.pl', write, S), lsp_formatter_parser:emit_reified(S, Src), close(S)). 339 | -------------------------------------------------------------------------------- /prolog/lsp_formatter_parser.pl: -------------------------------------------------------------------------------- 1 | :- module(lsp_formatter_parser, [ reified_format_for_file/2, 2 | emit_reified/2 ]). 3 | /** LSP Parser For Formatter 4 | 5 | Module for parsing Prolog source code, for subsequent formatting 6 | 7 | @author James Cash 8 | 9 | */ 10 | 11 | :- use_module(library(apply)). 12 | :- use_module(library(apply_macros)). 13 | :- use_module(library(clpfd)). 14 | :- use_module(library(rbtrees)). 15 | :- use_module(library(readutil), [ read_file_to_string/3 ]). 16 | 17 | :- include('_lsp_path_add.pl'). 18 | 19 | :- use_module(lsp(lsp_reading_source), [ file_lines_start_end/2, 20 | read_term_positions/2, 21 | file_offset_line_position/4 ]). 22 | 23 | :- thread_local current_source_string/1. 24 | 25 | %! reified_format_for_file(+Path:string, -Reified:list) is det. 26 | % 27 | % Read the prolog source file at Path into a flattened list of terms 28 | % indicating content, comments, and whitespace. 29 | reified_format_for_file(Path, Reified) :- 30 | retractall(current_source_string(_)), 31 | read_file_to_string(Path, FileString, []), 32 | read_term_positions(Path, TermsWithPos), 33 | setup_call_cleanup( 34 | assertz(current_source_string(FileString)), 35 | expand_term_positions(TermsWithPos, Reified0), 36 | retractall(current_source_string(_)) 37 | ), 38 | sort(1, @=<, Reified0, Reified1), 39 | file_lines_start_end(Path, LinesStartEnd), 40 | InitState = _{last_line: 1, last_char: 0, line_bounds: LinesStartEnd}, 41 | add_whitespace_terms(InitState, Reified1, Reified2), 42 | simplify_reified_terms(Reified2, Reified). 43 | 44 | % Remove no-longer needed positioning information to make things less 45 | % annoying for later steps. 46 | simplify_reified_terms(In, Out) :- 47 | maplist(simplify_reified_term, In, Out). 48 | 49 | simplify_reified_term(newline, newline) :- !. 50 | simplify_reified_term(white(N), white(N)) :- !. 51 | simplify_reified_term(Term, SimpleTerm) :- 52 | % all other terms have two extra args, From & To 53 | compound_name_arguments(Term, Name, [_, _|Args]), 54 | ( Args = [] 55 | -> SimpleTerm = Name 56 | ; compound_name_arguments(SimpleTerm, Name, Args) ). 57 | 58 | %! emit_reified(+To, +Reified) is det. 59 | % 60 | % Output source file as read with reified_format_for_file/2 to To, as 61 | % format/3. 62 | emit_reified(_, []) :- !. 63 | emit_reified(To, [Term|Rest]) :- 64 | emit_reified_(To, Term), 65 | emit_reified(To, Rest). 66 | 67 | emit_reified_(To, newline) => format(To, "~n", []). 68 | emit_reified_(To, white(N)) => 69 | length(Whites, N), 70 | maplist(=(0' ), Whites), 71 | format(To, "~s", [Whites]). 72 | emit_reified_(To, comma) => format(To, ",", []). 73 | emit_reified_(To, simple(T)) => 74 | format(To, "~s", [T]). 75 | emit_reified_(To, simple_quoted(T)) => 76 | format(To, "'~q'", [T]). 77 | emit_reified_(To, string(T)), string(T) => 78 | format(To, "~q", [T]). 79 | emit_reified_(To, string(T)) => 80 | % string term, but not a string, must be codes 81 | format(To, "`~s`", [T]). 82 | emit_reified_(To, term_begin(Func, _, Parens)) => 83 | ( Parens = true 84 | -> Format = "~w(" 85 | ; Format = "~w" ), 86 | format(To, Format, [Func]). 87 | emit_reified_(To, term_end(Parens, TermState)) => 88 | ( Parens = true 89 | -> MaybeClose = ")" 90 | ; MaybeClose = "" ), 91 | ( TermState = toplevel 92 | -> MaybeStop = "." 93 | ; MaybeStop = "" ), 94 | format(To, "~w~w", [MaybeClose, MaybeStop]). 95 | emit_reified_(To, list_begin) => 96 | format(To, "[", []). 97 | emit_reified_(To, list_tail) => 98 | format(To, "|", []). 99 | emit_reified_(To, list_end) => 100 | format(To, "]", []). 101 | emit_reified_(To, comment(Text)) => 102 | format(To, "~s", [Text]). 103 | emit_reified_(To, braces_begin) => 104 | format(To, "{", []). 105 | emit_reified_(To, braces_end) => 106 | format(To, "}", []). 107 | emit_reified_(To, parens_begin) => 108 | format(To, "(", []). 109 | emit_reified_(To, parens_end) => 110 | format(To, ")", []). 111 | emit_reified_(To, dict_tag('$var'(Tag))) => 112 | format(To, "~w", [Tag]). 113 | emit_reified_(To, dict_tag(Tag)), var(Tag) => 114 | % if Tag is still a var, it must be anonymous 115 | format(To, "_", []). 116 | emit_reified_(To, dict_tag(Tag)) => 117 | % if Tag is still a var, it must be anonymous 118 | format(To, "~w", [Tag]). 119 | emit_reified_(To, dict_begin) => 120 | format(To, "{", []). 121 | emit_reified_(To, dict_sep) => 122 | format(To, ":", []). 123 | emit_reified_(To, dict_end) => 124 | format(To, "}", []). 125 | 126 | %! add_whitespace_terms(+State:dict, +Reified:list, -Formatted:list) is det. 127 | % 128 | % Add terms indicating whitespace and newlines in between positioned 129 | % terms, as created by reified_format_for_file/2. 130 | add_whitespace_terms(_State, [], [newline]) :- !. 131 | add_whitespace_terms(State, [Term|Terms], Out) :- 132 | arg(1, Term, TermStart), 133 | stream_position_at_offset(State.line_bounds, TermStart, Pos), 134 | sync_position_whitespace(State, Pos, Out, Out1), 135 | Out1 = [Term|Out2], 136 | arg(2, Term, TermEnd), 137 | stream_position_at_offset(State.line_bounds, TermEnd, EndPos), 138 | update_state_position(State, EndPos, State1), 139 | add_whitespace_terms(State1, Terms, Out2). 140 | 141 | expand_term_positions([], []). 142 | expand_term_positions([InfoDict|Rest], Expanded0) :- 143 | ( InfoDict.comments \= [] 144 | -> expand_comments_positions(InfoDict.comments, Expanded0, Expanded1) 145 | ; Expanded1 = Expanded0 ), 146 | 147 | Term = InfoDict.term, 148 | ( Term \= end_of_file % just for comments at the end 149 | -> expand_subterm_positions(Term, toplevel, InfoDict.subterm, 150 | Expanded1, Expanded2) 151 | ; Expanded2 = Expanded1 ), 152 | 153 | expand_term_positions(Rest, Expanded2). 154 | 155 | expand_comments_positions([], Tail, Tail) :- !. 156 | expand_comments_positions([Comment|Rest], Expanded, Tail) :- 157 | expand_comment_positions(Comment, Expanded, Tail0), 158 | expand_comments_positions(Rest, Tail0, Tail). 159 | 160 | expand_comment_positions(CommentPos-Comment, Expanded, ExpandedTail) :- 161 | term_end_position(Comment, CommentEndPosRel), 162 | increment_stream_position(CommentPos, CommentEndPosRel, CommentEndPos), 163 | stream_position_data(char_count, CommentPos, From), 164 | stream_position_data(char_count, CommentEndPos, To), 165 | Expanded = [comment(From, To, Comment)|ExpandedTail]. 166 | 167 | expand_subterm_positions(Term, _TermState, term_position(_From, _To, FFrom, FTo, SubPoses), 168 | Expanded, ExTail), functor(Term, ',', _, _) => 169 | % special-case comma terms to be reified as commas 170 | Expanded = [comma(FFrom, FTo)|ExpandedTail0], 171 | functor(Term, _, Arity, _), 172 | expand_term_subterms_positions(false, Term, Arity, 1, SubPoses, ExpandedTail0, ExTail). 173 | expand_subterm_positions(Term, TermState, term_position(From, To, FFrom, FTo, SubPoses), 174 | Expanded, ExTail) => 175 | % using functor/4 to allow round-tripping zero-arity functors 176 | functor(Term, _Func, Arity, TermType), 177 | current_source_string(FileString), 178 | Length is FTo - FFrom, 179 | sub_string(FileString, FFrom, Length, _, FuncString), 180 | atom_string(Func, FuncString), 181 | % better way to tell if term is parenthesized? 182 | % read functor from current_source_string/1 (as with simple below) 183 | % and see if parens are there? 184 | ( From = FFrom, max_subterm_to(SubPoses, SubTermMax), To > SubTermMax 185 | -> ( Parens = true, FTo1 is FTo + 1 ) % add space for the parenthesis 186 | ; ( Parens = false, FTo1 = FTo ) ), 187 | Expanded = [term_begin(FFrom, FTo1, Func, TermType, Parens)|ExpandedTail0], 188 | expand_term_subterms_positions(Parens, Term, Arity, 1, SubPoses, 189 | ExpandedTail0, ExpandedTail1), 190 | succ(To0, To), 191 | ExpandedTail1 = [term_end(To0, To, Parens, TermState)|ExpandedTail2], 192 | maybe_add_comma(TermState, To, ExpandedTail2, ExTail). 193 | expand_subterm_positions(Term, TermState, string_position(From, To), Expanded, Tail) => 194 | Expanded = [string(From, To, Term)|Tail0], 195 | maybe_add_comma(TermState, To, Tail0, Tail). 196 | expand_subterm_positions(_Term, TermState, From-To, Expanded, Tail) => 197 | current_source_string(FileString), 198 | Length is To - From, 199 | sub_string(FileString, From, Length, _, SimpleString), 200 | Expanded = [simple(From, To, SimpleString)|Tail0], 201 | maybe_add_comma(TermState, To, Tail0, Tail). 202 | expand_subterm_positions(Term, TermState, list_position(From, To, Elms, HasTail), Expanded, Tail) => 203 | assertion(is_listish(Term)), 204 | ListBeginTo is From + 1, 205 | Expanded = [list_begin(From, ListBeginTo)|Expanded1], 206 | expand_list_subterms_positions(Term, Elms, Expanded1, Expanded2), 207 | succ(To0, To), 208 | ( HasTail = none 209 | -> Expanded2 = [list_end(To0, To)|Tail0] 210 | ; ( arg(1, HasTail, TailFrom), 211 | succ(TailBarFrom, TailFrom), 212 | Expanded2 = [list_tail(TailBarFrom, TailFrom)|Expanded3], 213 | list_tail(Term, Elms, ListTail), 214 | expand_subterm_positions(ListTail, false, HasTail, Expanded3, Expanded4), 215 | Expanded4 = [list_end(To0, To)|Tail0] ) ), 216 | maybe_add_comma(TermState, To, Tail0, Tail). 217 | expand_subterm_positions(Term, TermState, brace_term_position(From, To, BracesPos), Expanded, Tail) => 218 | BraceTo is From + 1, 219 | Expanded = [braces_begin(From, BraceTo)|Tail0], 220 | Term = {Term0}, 221 | expand_subterm_positions(Term0, false, BracesPos, Tail0, Tail1), 222 | succ(To1, To), 223 | Tail1 = [braces_end(To1, To)|Tail2], 224 | maybe_add_comma(TermState, To1, Tail2, Tail). 225 | expand_subterm_positions(Term, TermState, parentheses_term_position(From, To, ContentPos), 226 | Expanded, Tail) => 227 | ParenTo is From + 1, 228 | Expanded = [parens_begin(From, ParenTo)|Tail0], 229 | expand_subterm_positions(Term, false, ContentPos, Tail0, Tail1), 230 | succ(To1, To), 231 | Tail1 = [parens_end(To1, To)|Tail2], 232 | maybe_add_comma(TermState, To, Tail2, Tail). 233 | expand_subterm_positions(Term, TermState, dict_position(_From, To, TagFrom, TagTo, KeyValPos), 234 | Expanded, Tail) => 235 | is_dict(Term, Tag), 236 | DictBraceTo is TagTo + 1, 237 | Expanded = [dict_tag(TagFrom, TagTo, Tag), dict_begin(TagTo, DictBraceTo)|Tail0], 238 | expand_dict_kvs_positions(Term, KeyValPos, Tail0, Tail1), 239 | succ(To1, To), 240 | Tail1 = [dict_end(To1, To)|Tail2], 241 | maybe_add_comma(TermState, To, Tail2, Tail). 242 | 243 | maybe_add_comma(subterm_item, CommaFrom, Tail0, Tail) :- !, 244 | CommaTo is CommaFrom + 1, 245 | Tail0 = [comma(CommaFrom, CommaTo)|Tail]. 246 | maybe_add_comma(_, _, Tail, Tail). 247 | 248 | is_listish(L) :- \+ var(L), !. 249 | is_listish([]). 250 | is_listish([_|_]). 251 | 252 | list_tail(Tail, [], Tail) :- !. 253 | list_tail([_|Rest], [_|PosRest], Tail) :- 254 | list_tail(Rest, PosRest, Tail). 255 | 256 | max_subterm_to(SubPoses, SubTermMaxTo) :- 257 | aggregate_all(max(To), 258 | ( member(Pos, SubPoses), 259 | arg(2, Pos, To) ), 260 | SubTermMaxTo). 261 | 262 | expand_dict_kvs_positions(_, [], Tail, Tail) :- !. 263 | expand_dict_kvs_positions(Dict, [Pos|Poses], Expanded0, Tail) :- 264 | Pos = key_value_position(_From, To, SepFrom, SepTo, Key, KeyPos, ValuePos), 265 | get_dict(Key, Dict, Value), 266 | expand_subterm_positions(Key, false, KeyPos, Expanded0, Expanded1), 267 | Expanded1 = [dict_sep(SepFrom, SepTo)|Expanded2], 268 | expand_subterm_positions(Value, false, ValuePos, Expanded2, Expanded3), 269 | CommaTo is To + 1, 270 | ( Poses = [_|_] 271 | -> Expanded3 = [comma(To, CommaTo)|Expanded4] 272 | ; Expanded3 = Expanded4 ), 273 | expand_dict_kvs_positions(Dict, Poses, Expanded4, Tail). 274 | 275 | % possible for the list to still have a tail when out of positions 276 | expand_list_subterms_positions(_, [], Tail, Tail) :- !. 277 | expand_list_subterms_positions([Term|Terms], [Pos|Poses], Expanded, Tail) :- 278 | ( Poses = [_|_] 279 | -> TermState = subterm_item 280 | ; TermState = false ), 281 | expand_subterm_positions(Term, TermState, Pos, Expanded, Expanded1), 282 | expand_list_subterms_positions(Terms, Poses, Expanded1, Tail). 283 | 284 | expand_term_subterms_positions(_Parens, _Term, _Arity, _Arg, [], Tail, Tail) :- !. 285 | expand_term_subterms_positions(Parens, Term, Arity, Arg, [SubPos|Poses], Expanded, ExpandedTail) :- 286 | assertion(between(1, Arity, Arg)), 287 | arg(Arg, Term, SubTerm), 288 | ( Parens = true, Arg < Arity 289 | -> State = subterm_item 290 | ; State = false ), 291 | expand_subterm_positions(SubTerm, State, SubPos, Expanded, Expanded0), 292 | succ(Arg, Arg1), 293 | expand_term_subterms_positions(Parens, Term, Arity, Arg1, Poses, Expanded0, ExpandedTail). 294 | 295 | increment_stream_position(StartPos, RelPos, EndPos) :- 296 | stream_position_data(char_count, StartPos, StartCharCount), 297 | stream_position_data(char_count, RelPos, RelCharCount), 298 | CharCount is StartCharCount + RelCharCount, 299 | stream_position_data(byte_count, StartPos, StartByteCount), 300 | stream_position_data(byte_count, RelPos, RelByteCount), 301 | ByteCount is StartByteCount + RelByteCount, 302 | stream_position_data(line_count, StartPos, StartLineCount), 303 | stream_position_data(line_count, RelPos, RelLineCount), 304 | stream_position_data(line_position, StartPos, StartLinePosition), 305 | stream_position_data(line_position, RelPos, RelLinePosition), 306 | ( RelLineCount == 1 307 | -> LineCount = StartLineCount, 308 | LinePosition is StartLinePosition + RelLinePosition 309 | ; ( LineCount is StartLineCount + RelLineCount - 1, 310 | LinePosition = RelLinePosition ) ), 311 | EndPos = '$stream_position_data'(CharCount, LineCount, LinePosition, ByteCount). 312 | 313 | update_state_position(State0, EndPos, State2) :- 314 | stream_position_data(line_count, EndPos, EndLineCount), 315 | stream_position_data(line_position, EndPos, EndLinePos), 316 | put_dict(last_line, State0, EndLineCount, State1), 317 | put_dict(last_char, State1, EndLinePos, State2). 318 | 319 | sync_position_whitespace(State, TermPos, Expanded, ExpandedTail) :- 320 | PrevLineCount = State.last_line, 321 | stream_position_data(line_count, TermPos, NewLineCount), 322 | NewLines is NewLineCount - PrevLineCount, 323 | ( NewLines > 0 324 | -> n_copies_of(NewLines, newline, Expanded, Expanded0), 325 | PrevLinePosition = 0 326 | ; ( Expanded = Expanded0, 327 | PrevLinePosition = State.last_char ) 328 | ), 329 | 330 | stream_position_data(line_position, TermPos, NewLinePosition), 331 | Whitespace is NewLinePosition - PrevLinePosition, 332 | ( Whitespace > 0 333 | -> Expanded0 = [white(Whitespace)|ExpandedTail] 334 | ; Expanded0 = ExpandedTail ). 335 | 336 | %! stream_position_at_offset(+LineCharMap:rbtree, +Offset:Int, -Pos) is det. 337 | stream_position_at_offset(LineCharMap, To, EndPos) :- 338 | CharCount = To, 339 | ByteCount = To, % need to check for multibyte... 340 | file_offset_line_position(LineCharMap, To, LineCount, LinePosition), 341 | % breaking the rules, building an opaque term 342 | EndPos = '$stream_position_data'(CharCount, LineCount, LinePosition, ByteCount). 343 | 344 | % Helpers 345 | 346 | term_end_position(Term, Position) :- 347 | setup_call_cleanup( 348 | open_null_stream(Out), 349 | ( write(Out, Term), 350 | stream_property(Out, position(Position)) 351 | ), 352 | close(Out)). 353 | 354 | n_copies_of(0, _, Tail, Tail) :- !. 355 | n_copies_of(N, ToCopy, [ToCopy|Rest], Tail) :- 356 | N1 is N - 1, 357 | n_copies_of(N1, ToCopy, Rest, Tail). 358 | -------------------------------------------------------------------------------- /prolog/lsp_highlights.pl: -------------------------------------------------------------------------------- 1 | :- module(lsp_highlights, [ highlights_at_position/3 ]). 2 | 3 | :- include('_lsp_path_add.pl'). 4 | 5 | :- use_module(library(apply), [maplist/2]). 6 | :- use_module(library(apply_macros)). 7 | :- use_module(library(yall)). 8 | 9 | :- use_module(lsp(lsp_reading_source)). 10 | 11 | highlights_at_position(Path, Position, Highlights) :- 12 | highlights_at_position(Path, Position, _, Highlights). 13 | 14 | highlights_at_position(Path, line_char(Line1, Char0), Leaf, Highlights) :- 15 | file_lines_start_end(Path, LineCharRange), 16 | read_term_positions(Path, TermsWithPositions), 17 | % find the top-level term that the offset falls within 18 | file_offset_line_position(LineCharRange, Offset, Line1, Char0), 19 | % find the specific sub-term containing the point 20 | member(TermInfo, TermsWithPositions), 21 | SubTermPoses = TermInfo.subterm, 22 | arg(1, SubTermPoses, TermFrom), 23 | arg(2, SubTermPoses, TermTo), 24 | between(TermFrom, TermTo, Offset), !, 25 | subterm_leaf_position(TermInfo.term, Offset, SubTermPoses, Leaf), 26 | ( Leaf = '$var'(_) 27 | % if it's a variable, only look inside the containing term 28 | -> find_occurrences_of_var(Leaf, TermInfo, Matches) 29 | % if it's the functor of a term, find all occurrences in the file 30 | ; functor(Leaf, FuncName, Arity), 31 | find_occurrences_of_func(FuncName, Arity, TermsWithPositions, Matches) 32 | ), 33 | maplist(position_to_match(LineCharRange), Matches, Highlights). 34 | 35 | find_occurrences_of_func(FuncName, Arity, TermInfos, Matches) :- 36 | find_occurrences_of_func(FuncName, Arity, TermInfos, Matches, []). 37 | find_occurrences_of_func(_, _, [], Tail, Tail). 38 | find_occurrences_of_func(FuncName, Arity, [TermInfo|Rest], Matches, Tail) :- 39 | find_in_term_with_positions({FuncName, Arity}/[X, _]>>( nonvar(X), 40 | functor(X, FuncName, Arity) ), 41 | TermInfo.term, TermInfo.subterm, Matches, Tail0), 42 | find_occurrences_of_func(FuncName, Arity, Rest, Tail0, Tail). 43 | 44 | find_occurrences_of_var(Var, TermInfo, Matches) :- 45 | Var = '$var'(Name), ground(Name), % wrapped term; otherwise it's anonymous & matches nothing 46 | Term = TermInfo.term, 47 | Poses = TermInfo.subterm, 48 | find_in_term_with_positions({Var}/[X, _]>>( ground(X), X = Var ), Term, Poses, 49 | Matches, []). 50 | -------------------------------------------------------------------------------- /prolog/lsp_parser.pl: -------------------------------------------------------------------------------- 1 | :- module(lsp_parser, [lsp_request//1]). 2 | /** LSP Parser 3 | 4 | Module for parsing the body & headers from an LSP client. 5 | 6 | @author James Cash 7 | */ 8 | 9 | :- use_module(library(assoc), [list_to_assoc/2, get_assoc/3]). 10 | :- use_module(library(codesio), [open_codes_stream/2]). 11 | :- use_module(library(dcg/basics), [string_without//2]). 12 | :- use_module(library(http/json), [json_read_dict/3]). 13 | 14 | header(Key-Value) --> 15 | string_without(":", KeyC), ": ", string_without("\r", ValueC), 16 | { string_codes(Key, KeyC), string_codes(Value, ValueC) }. 17 | 18 | headers([Header]) --> 19 | header(Header), "\r\n\r\n", !. 20 | headers([Header|Headers]) --> 21 | header(Header), "\r\n", 22 | headers(Headers). 23 | 24 | %! lsp_request(-Req)// is det. 25 | % 26 | % A DCG nonterminal describing an HTTP request. Currently can only _parse_ 27 | % the request from a list of codes. 28 | % 29 | % The HTTP headers are parsed into an `assoc` tree which maps 30 | % strings to strings. The body of the request is parsed into a dict according 31 | % to json_read_dict/3. The headers list must include a `Content-Length` 32 | % header. 33 | % 34 | % == 35 | % ?- phrase(lsp_request(Req), `Content-Length: 7\r\n\r\n{"x":1}`). 36 | % Req = _{body:_{x:1}, headers:t("Content-Length", "7", -, t, t)}. 37 | % == 38 | % 39 | % @param Req a dict containing keys `headers` and `body` 40 | lsp_request(_{headers: Headers, body: Body}) --> 41 | headers(HeadersList), 42 | { list_to_assoc(HeadersList, Headers), 43 | get_assoc("Content-Length", Headers, LengthS), 44 | number_string(Length, LengthS), 45 | length(JsonCodes, Length) }, 46 | JsonCodes, 47 | { ground(JsonCodes), 48 | open_codes_stream(JsonCodes, JsonStream), 49 | json_read_dict(JsonStream, Body, []) }. 50 | -------------------------------------------------------------------------------- /prolog/lsp_reading_source.pl: -------------------------------------------------------------------------------- 1 | :- module(lsp_reading_source, [ file_lines_start_end/2, 2 | read_term_positions/2, 3 | read_term_positions/4, 4 | file_offset_line_position/4, 5 | find_in_term_with_positions/5, 6 | position_to_match/3, 7 | subterm_leaf_position/4 8 | ]). 9 | /** LSP Reading Source 10 | 11 | Module for reading in Prolog source code with positions, mostly 12 | wrapping prolog_read_source_term/4. 13 | 14 | @author James Cash 15 | 16 | @tbd Files using quasi-quotations currently aren't supported; need to 17 | teach prolog_read_source_term/4 to load correctly 18 | 19 | */ 20 | 21 | :- use_module(library(apply)). 22 | :- use_module(library(apply_macros)). 23 | :- use_module(library(clpfd)). 24 | :- use_module(library(prolog_source)). 25 | :- use_module(library(readutil), [ read_line_to_codes/2 ]). 26 | :- use_module(library(yall)). 27 | 28 | %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% 29 | % Specialized reading predicates 30 | %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% 31 | 32 | %! file_lines_start_end(+Path:text, -LineCharRange:list) is det. 33 | % 34 | % Construct a mapping of file offsets to line numbers in the file at 35 | % Path. LineCharRange will be a list containing terms like 36 | % =line_start_end(LineNumber, LineOffsetStart, LineOffsetEnd)= 37 | file_lines_start_end(Path, LineCharRange) :- 38 | Acc = line_data([], line(1, 0)), 39 | setup_call_cleanup( 40 | open(Path, read, Stream), 41 | ( repeat, 42 | read_line_to_codes(Stream, Line), 43 | stream_property(Stream, position(Position)), 44 | stream_position_data(char_count, Position, NewLineStart), 45 | arg(2, Acc, line(LastLine, LastLineStart)), 46 | arg(1, Acc, Data), 47 | LastLineEnd is NewLineStart - 1, 48 | nb_setarg(1, Acc, [(LastLineStart-LastLineEnd)-LastLine|Data]), 49 | NextLine is LastLine + 1, 50 | nb_setarg(2, Acc, line(NextLine, NewLineStart)), 51 | Line == end_of_file, ! 52 | ), 53 | close(Stream)), 54 | arg(1, Acc, Ranges), 55 | list_to_rbtree(Ranges, RangeToLine), 56 | maplist([Range-Line, Line-Range]>>true, Ranges, InvRanges), 57 | list_to_rbtree(InvRanges, LineToRange), 58 | LineCharRange = RangeToLine-LineToRange. 59 | 60 | %! read_term_positions(+Path:text, -TermsWithPositions:list) is det. 61 | % 62 | % Read in all the terms in the file at Path, using 63 | % prolog_read_source_term/4, to a list of dictionaries. 64 | % Each dictionary has the following keys: 65 | % * term 66 | % The term read in, with variables replace with the term '$var'(VariableName). 67 | % * pos 68 | % The position of the term (see [[prolog_read_source_term/4]]). 69 | % * subterm 70 | % The position of the subterms in term (see [[prolog_read_source_term/4]]). 71 | % * variable_names 72 | % List of Name=Var terms for the variables in Term. Note that the 73 | % variables in term have already been replace with var(Name) 74 | % * comments 75 | % Comments in the term, with the same format as prolog_read_source_term/4 76 | read_term_positions(Path, TermsWithPositions) :- 77 | Acc = data([]), 78 | prolog_canonical_source(Path, SourceId), 79 | setup_call_cleanup( 80 | prolog_open_source(SourceId, Stream), 81 | ( repeat, 82 | prolog_read_source_term(Stream, Term, _Ex, [term_position(TermPos), 83 | subterm_positions(SubTermPos), 84 | variable_names(VarNames), 85 | comments(Comments), 86 | % maybe use `error` for running standalone? 87 | syntax_errors(dec10)]), 88 | maplist([Name=Var]>>( Var = '$var'(Name) ), VarNames), 89 | arg(1, Acc, Lst), 90 | nb_setarg(1, Acc, [_{term: Term, pos: TermPos, subterm: SubTermPos, 91 | varible_names: VarNames, comments: Comments}|Lst]), 92 | Term = end_of_file, ! 93 | ), 94 | prolog_close_source(Stream)), 95 | arg(1, Acc, TermsWithPositionsRev), 96 | reverse(TermsWithPositionsRev, TermsWithPositions). 97 | 98 | %! read_term_positions(+Path:text, +Start:integer, +End:integer, -TermsWithPositions:list) is det. 99 | % 100 | % Read in all the terms in the file at Path between Start and End, using 101 | % prolog_read_source_term/4, to a list of dictionaries. 102 | % Each dictionary has the following keys: 103 | % * term 104 | % The term read in, with variables replace with the term '$var'(VariableName). 105 | % * pos 106 | % The position of the term (see [[prolog_read_source_term/4]]). 107 | % * subterm 108 | % The position of the subterms in term (see [[prolog_read_source_term/4]]). 109 | % * variable_names 110 | % List of Name=Var terms for the variables in Term. Note that the 111 | % variables in term have already been replace with var(Name) 112 | % * comments 113 | % Comments in the term, with the same format as prolog_read_source_term/4 114 | read_term_positions(Path, Start, End, TermsWithPositions) :- 115 | Acc = data([]), 116 | prolog_canonical_source(Path, SourceId), 117 | setup_call_cleanup( 118 | prolog_open_source(SourceId, Stream), 119 | ( repeat, 120 | prolog_read_source_term(Stream, Term, _Ex, [term_position(TermPos), 121 | subterm_positions(SubTermPos), 122 | variable_names(VarNames), 123 | comments(Comments), 124 | % maybe use `error` for running standalone? 125 | syntax_errors(dec10)]), 126 | arg(1, SubTermPos, TermStart), 127 | TermStart >= Start, 128 | maplist([Name=Var]>>( Var = '$var'(Name) ), VarNames), 129 | arg(1, Acc, Lst), 130 | nb_setarg(1, Acc, [_{term: Term, pos: TermPos, subterm: SubTermPos, 131 | varible_names: VarNames, comments: Comments}|Lst]), 132 | arg(2, SubTermPos, TermEnd), 133 | once(( Term = end_of_file ; TermEnd >= End )), ! 134 | ), 135 | prolog_close_source(Stream)), 136 | arg(1, Acc, TermsWithPositionsRev), 137 | reverse(TermsWithPositionsRev, TermsWithPositions). 138 | 139 | 140 | %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% 141 | % Using LineCharMap 142 | %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% 143 | 144 | %! file_offset_line_position(+LineCharMap:term, ?Offset:integer, ?Line:integer, ?Column:integer) is semidet. 145 | % 146 | % True when LineCharMap is a term as created by 147 | % file_lines_start_end/2, Offset is the offset into the file, Line is 148 | % the line number and Column is the character within that line. 149 | % 150 | % Presumably either Offset is ground or Line & Column are. 151 | file_offset_line_position(LineCharMap-_, CharCount, Line, LinePosition) :- 152 | ground(CharCount), !, 153 | rb_lookup_range(CharCount, Start-_End, Line, LineCharMap), 154 | LinePosition #= CharCount - Start. 155 | file_offset_line_position(_-LineCharMap, CharCount, Line, LinePosition) :- 156 | rb_lookup(Line, Start-_End, LineCharMap), 157 | CharCount #= Start + LinePosition. 158 | 159 | %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% 160 | % Red-black trees helper 161 | %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% 162 | rb_lookup_range(Key, KeyRange, Value, t(_, Tree)) => 163 | rb_lookup_range_(Key, KeyRange, Value, Tree). 164 | 165 | rb_lookup_range_(_Key, _KeyRange, _Value, black('', _, _, '')) :- !, fail. 166 | rb_lookup_range_(Key, KeyRange, Value, Tree) :- 167 | arg(2, Tree, Start-End), 168 | compare(CmpS, Key, Start), 169 | compare(CmpE, Key, End), 170 | rb_lookup_range_(t(CmpS, CmpE), Key, Start-End, KeyRange, Value, Tree). 171 | 172 | rb_lookup_range_(t(>, <), _, Start-End, KeyRange, Value, Tree) => 173 | arg(3, Tree, Value), 174 | KeyRange = Start-End. 175 | rb_lookup_range_(t(=, _), _, Start-End, KeyRange, Value, Tree) => 176 | arg(3, Tree, Value), 177 | KeyRange = Start-End. 178 | rb_lookup_range_(t(_, =), _, Start-End, KeyRange, Value, Tree) => 179 | arg(3, Tree, Value), 180 | KeyRange = Start-End. 181 | rb_lookup_range_(t(<, _), Key, _, KeyRange, Value, Tree) => 182 | arg(1, Tree, NTree), 183 | rb_lookup_range_(Key, KeyRange, Value, NTree). 184 | rb_lookup_range_(t(_, >), Key, _, KeyRange, Value, Tree) => 185 | arg(4, Tree, NTree), 186 | rb_lookup_range_(Key, KeyRange, Value, NTree). 187 | 188 | %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% 189 | % Searching through read results 190 | %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% 191 | 192 | position_to_match(LineCharRange, found_at(_, From-To), Match) :- !, 193 | file_offset_line_position(LineCharRange, From, FromLine1, FromCharacter), 194 | file_offset_line_position(LineCharRange, To, ToLine1, ToCharacter), 195 | succ(FromLine0, FromLine1), 196 | succ(ToLine0, ToLine1), 197 | Match = _{range: _{start: _{line: FromLine0, character: FromCharacter}, 198 | end: _{line: ToLine0, character: ToCharacter}}}. 199 | position_to_match(LineCharRange, found_at(_, term_position(_, _, FFrom, FTo, _)), Match) :- 200 | file_offset_line_position(LineCharRange, FFrom, FromLine1, FromCharacter), 201 | file_offset_line_position(LineCharRange, FTo, ToLine1, ToCharacter), 202 | succ(FromLine0, FromLine1), 203 | succ(ToLine0, ToLine1), 204 | Match = _{range: _{start: _{line: FromLine0, character: FromCharacter}, 205 | end: _{line: ToLine0, character: ToCharacter}}}. 206 | 207 | :- meta_predicate find_in_term_with_positions(2, +, +, -, -). 208 | 209 | %! find_in_term_with_positions(+Search:callable, +Term, +Positions, -Matches, -Tail) is det. 210 | % 211 | % True when Search is a callable that takes two arguments, a term and its position, and is 212 | % true if the term should be included in match, Term is the term in 213 | % which to search, Positions is the subterm positions as given from read_term_positions/2, 214 | % Matches is a list of the found matching terms, and Tail is the tail of the Matches list. 215 | find_in_term_with_positions(Needle, Term, Position, Matches, Tail) :- 216 | call(Needle, Term, Position), !, % recurse? 217 | Matches = [found_at(Term, Position)|Tail]. 218 | find_in_term_with_positions(Needle, Term, term_position(_, _, _, _, SubPoses), Matches, Tail) :- !, 219 | find_in_term_subterm(Needle, Term, 1, SubPoses, Matches, Tail). 220 | find_in_term_with_positions(Needle, Term, list_position(_, _, Elms, TailPos), Matches, Tail) :- !, 221 | find_in_term_list(Needle, Term, Elms, TailPos, Matches, Tail). 222 | find_in_term_with_positions(Needle, Term, brace_term_position(_, _, ArgPos), Matches, Tail) :- !, 223 | Term = {Term0}, 224 | find_in_term_with_positions(Needle, Term0, ArgPos, Matches, Tail). 225 | find_in_term_with_positions(Needle, Term, parentheses_term_position(_, _, ContentPos), Matches, Tail) :- !, 226 | find_in_term_with_positions(Needle, Term, ContentPos, Matches, Tail). 227 | find_in_term_with_positions(Needle, Term, dict_position(_, _, _, _, ContentPos), Matches, Tail) :- !, 228 | find_in_term_dict(Needle, Term, ContentPos, Matches, Tail). 229 | find_in_term_with_positions(_, _Term, _Pos, Tail, Tail). 230 | 231 | find_in_term_dict(_, _, [], Tail, Tail) :- !. 232 | find_in_term_dict(Needle, Term, [Pos|Poses], Matches, Tail) :- 233 | key_value_position(_KVFrom, _KVTo, _SF, _ST, Key, _KeyPos, ValuePos) = Pos, 234 | get_dict(Key, Term, Value), 235 | find_in_term_with_positions(Needle, Value, ValuePos, Matches, Tail0), 236 | find_in_term_dict(Needle, Term, Poses, Tail0, Tail). 237 | 238 | find_in_term_list(_, _, [], none, Tail, Tail) :- !. 239 | find_in_term_list(Needle, TailElt, [], TailPos, Matches, Tail) :- !, 240 | find_in_term_with_positions(Needle, TailElt, TailPos, Matches, Tail). 241 | find_in_term_list(Needle, [X|Xs], [Pos|Poses], TailPos, Matches, Tail) :- 242 | find_in_term_with_positions(Needle, X, Pos, Matches, Tail0), 243 | find_in_term_list(Needle, Xs, Poses, TailPos, Tail0, Tail). 244 | 245 | find_in_term_subterm(_, _, _, [], Tail, Tail) :- !. 246 | find_in_term_subterm(Needle, Term, Arg, [Position|Positions], Matches, Tail) :- 247 | arg(Arg, Term, SubTerm), 248 | NextArg is Arg + 1, 249 | find_in_term_with_positions(Needle, SubTerm, Position, Matches, Matches0), 250 | find_in_term_subterm(Needle, Term, NextArg, Positions, Matches0, Tail). 251 | 252 | %! between_semi(?Low:integer, ?High:integer, ?X:integer) is semidet. 253 | % 254 | % Like between/3, except with a semi-closed interval, not including =High=. 255 | between_semi(Lo, High, X) :- 256 | HighInc #= High - 1, 257 | between(Lo, HighInc, X). 258 | 259 | %! subterm_leaf_position(+Term, +Offset, +SubTermPoses, ?Leaf) is semidet. 260 | subterm_leaf_position(Term, Offset, From-To, Term) :- between_semi(From, To, Offset), !. 261 | subterm_leaf_position(Term, Offset, term_position(_, _, FFrom, FTo, _), Term) :- 262 | between_semi(FFrom, FTo, Offset), !. 263 | subterm_leaf_position(Term, Offset, term_position(From, To, _, _, Subterms), Leaf) :- 264 | between_semi(From, To, Offset), !, 265 | functor(Term, _, Arity, _), 266 | between(1, Arity, Arg), 267 | arg(Arg, Term, Subterm), 268 | nth1(Arg, Subterms, SubtermPos), 269 | subterm_leaf_position(Subterm, Offset, SubtermPos, Leaf), !. 270 | subterm_leaf_position(Term, Offset, list_position(From, To, Elms, _), Leaf) :- 271 | between_semi(From, To, Offset), 272 | length(Elms, NElms), 273 | between(1, NElms, Idx), 274 | nth1(Idx, Term, Elm), 275 | nth1(Idx, Elms, ElmPos), 276 | subterm_leaf_position(Elm, Offset, ElmPos, Leaf), !. 277 | subterm_leaf_position(Term, Offset, list_position(From, To, Elms, TailPos), Leaf) :- 278 | between_semi(From, To, Offset), TailPos \= none, !, 279 | length(Elms, NElms), 280 | length(Head, NElms), 281 | append(Head, Tail, Term), 282 | subterm_leaf_position(Tail, Offset, TailPos, Leaf), !. 283 | subterm_leaf_position(Term, Offset, brace_term_position(From, To, BracesPos), Leaf) :- 284 | between_semi(From, To, Offset), !, 285 | Term = {Term0}, 286 | subterm_leaf_position(Term0, Offset, BracesPos, Leaf). 287 | subterm_leaf_position(Term, Offset, parentheses_term_position(From, To, ContentPos), Leaf) :- 288 | between_semi(From, To, Offset), !, 289 | subterm_leaf_position(Term, Offset, ContentPos, Leaf). 290 | subterm_leaf_position(Term, Offset, dict_position(_From, _To, TagFrom, TagTo, _KVPoses), Leaf) :- 291 | between_semi(TagFrom, TagTo, Offset), !, 292 | is_dict(Term, Leaf). 293 | subterm_leaf_position(Term, Offset, dict_position(From, To, _TagFrom, _TagTo, KVPoses), Leaf) :- 294 | between_semi(From, To, Offset), !, 295 | member(key_value_position(KVFrom, KVTo, _SF, _ST, Key, _KeyPos, ValuePos), KVPoses), 296 | between_semi(KVFrom, KVTo, Offset), !, 297 | % keys of a literal dict aren't of interest, I think? 298 | get_dict(Key, Term, Value), 299 | subterm_leaf_position(Value, Offset, ValuePos, Leaf). 300 | -------------------------------------------------------------------------------- /prolog/lsp_server.pl: -------------------------------------------------------------------------------- 1 | :- module(lsp_server, [main/0]). 2 | /** LSP Server 3 | 4 | The main entry point for the Language Server implementation. 5 | 6 | @author James Cash 7 | */ 8 | 9 | :- use_module(library(apply), [maplist/2]). 10 | :- use_module(library(apply_macros)). 11 | :- use_module(library(debug), [debug/3, debug/1]). 12 | :- use_module(library(http/json), [atom_json_dict/3]). 13 | :- use_module(library(prolog_xref)). 14 | :- use_module(library(prolog_source), [directory_source_files/3]). 15 | :- use_module(library(utf8), [utf8_codes//1]). 16 | :- use_module(library(socket), [tcp_socket/1, 17 | tcp_bind/2, 18 | tcp_accept/3, 19 | tcp_listen/2, 20 | tcp_open_socket/2]). 21 | :- use_module(library(yall)). 22 | :- use_module(library(prolog_stack)). 23 | 24 | :- include('_lsp_path_add.pl'). 25 | 26 | :- use_module(lsp(lsp_utils)). 27 | :- use_module(lsp(lsp_checking), [check_errors/2]). 28 | :- use_module(lsp(lsp_parser), [lsp_request//1]). 29 | :- use_module(lsp(lsp_changes), [handle_doc_changes/2]). 30 | :- use_module(lsp(lsp_completion), [completions_at/3]). 31 | :- use_module(lsp(lsp_colours), [file_colours/2, 32 | file_range_colours/4, 33 | token_types/1, 34 | token_modifiers/1]). 35 | :- use_module(lsp(lsp_formatter), [file_format_edits/2]). 36 | :- use_module(lsp(lsp_highlights), [highlights_at_position/3]). 37 | 38 | main :- 39 | set_prolog_flag(debug_on_error, false), 40 | set_prolog_flag(report_error, true), 41 | set_prolog_flag(verbose, silent), 42 | set_prolog_flag(toplevel_prompt, ''), 43 | current_prolog_flag(argv, Args), 44 | debug(server), 45 | start(Args). 46 | 47 | start([stdio]) :- !, 48 | debug(server, "Starting stdio client", []), 49 | stdio_server. 50 | start([port, Port]) :- !, 51 | debug(server, "Starting socket client on port ~w", [Port]), 52 | atom_number(Port, PortN), 53 | socket_server(PortN). 54 | start(Args) :- 55 | debug(server, "Unknown args ~w", [Args]). 56 | 57 | :- dynamic shutdown_request_received/0. 58 | :- dynamic exit_request_received/0. 59 | 60 | % stdio server 61 | 62 | stdio_server :- 63 | current_input(In), 64 | current_output(Out), 65 | stream_pair(StreamPair, In, Out), 66 | handle_requests_stream(StreamPair). 67 | 68 | % socket server 69 | socket_server(Port) :- 70 | tcp_socket(Socket), 71 | tcp_bind(Socket, Port), 72 | tcp_listen(Socket, 5), 73 | tcp_open_socket(Socket, StreamPair), 74 | stream_pair(StreamPair, AcceptFd, _), 75 | dispatch_socket_client(AcceptFd). 76 | 77 | dispatch_socket_client(AcceptFd) :- 78 | tcp_accept(AcceptFd, Socket, Peer), 79 | % not doing this in a thread and not looping 80 | % since it doesn't really make sense to have multiple clients connected 81 | process_client(Socket, Peer). 82 | 83 | process_client(Socket, Peer) :- 84 | setup_call_cleanup( 85 | tcp_open_socket(Socket, StreamPair), 86 | ( debug(server, "Connecting new client ~w", [Peer]), 87 | handle_requests_stream(StreamPair) ), 88 | close(StreamPair)). 89 | 90 | % common stream handler 91 | 92 | handle_requests_stream(StreamPair) :- 93 | stream_pair(StreamPair, In, Out), 94 | set_stream(In, buffer(full)), 95 | set_stream(In, newline(posix)), 96 | set_stream(In, tty(false)), 97 | set_stream(In, representation_errors(error)), 98 | % handling UTF decoding in JSON parsing, but doing the auto-translation 99 | % causes Content-Length to be incorrect 100 | set_stream(In, encoding(octet)), 101 | set_stream(Out, encoding(utf8)), 102 | client_handler(A-A, In, Out). 103 | 104 | client_handler(Extra-ExtraTail, In, Out) :- 105 | wait_for_input([In], Ready, 1.0), 106 | ( exit_request_received 107 | -> debug(server(high), "ending client handler loop", []) 108 | ; ( Ready =@= [] 109 | -> client_handler(Extra-ExtraTail, In, Out) 110 | ; fill_buffer(In), 111 | read_pending_codes(In, ReadCodes, Tail), 112 | ( Tail == [] 113 | -> true 114 | ; ExtraTail = ReadCodes, 115 | handle_requests(Out, Extra, Remainder), 116 | client_handler(Remainder-Tail, In, Out) ) ) ). 117 | 118 | % [TODO] add multithreading? Guess that will also need a message queue 119 | % to write to stdout 120 | handle_requests(Out, In, Tail) :- 121 | phrase(lsp_request(Req), In, Rest), !, 122 | ignore(handle_request(Req, Out)), 123 | ( var(Rest) 124 | -> Tail = Rest 125 | ; handle_requests(Out, Rest, Tail) ). 126 | handle_requests(_, T, T). 127 | 128 | % general handling stuff 129 | 130 | send_message(Stream, Msg) :- 131 | put_dict(jsonrpc, Msg, "2.0", VersionedMsg), 132 | atom_json_dict(JsonCodes, VersionedMsg, [as(codes), width(0)]), 133 | phrase(utf8_codes(JsonCodes), UTF8Codes), 134 | length(UTF8Codes, ContentLength), 135 | format(Stream, "Content-Length: ~w\r\n\r\n~s", [ContentLength, JsonCodes]), 136 | flush_output(Stream). 137 | 138 | handle_request(Req, OutStream) :- 139 | debug(server(high), "Request ~w", [Req.body]), 140 | catch_with_backtrace( 141 | ( ( shutdown_request_received 142 | -> ( Req.body.method == "exit" 143 | -> handle_msg(Req.body.method, Req.body, _Resp) 144 | ; send_message(OutStream, _{id: Req.body.id, error: _{code: -32600, message: "Invalid Request"}}) ) 145 | ; ( handle_msg(Req.body.method, Req.body, Resp) 146 | -> true 147 | ; throw(error(domain_error(handleable_message, Req), 148 | context(_Loc, "handle_msg/3 returned false"))) ), 149 | ( is_dict(Resp) -> send_message(OutStream, Resp) ; true ) ) ), 150 | Err, 151 | ( print_message(error, Err), 152 | get_dict(id, Req.body, Id), 153 | send_message(OutStream, _{id: Id, 154 | error: _{code: -32001, 155 | message: "server error"}}) 156 | )). 157 | 158 | % Handling messages 159 | 160 | server_capabilities(_{textDocumentSync: _{openClose: true, 161 | change: 2, %incremental 162 | save: _{includeText: false}, 163 | willSave: false, 164 | willSaveWaitUntil: false}, 165 | hoverProvider: true, 166 | completionProvider: _{}, 167 | definitionProvider: true, 168 | declarationProvider: true, 169 | implementationProvider: true, 170 | referencesProvider: true, 171 | documentHighlightProvider: true, 172 | documentSymbolProvider: true, 173 | workspaceSymbolProvider: true, 174 | codeActionProvider: false, 175 | %% codeLensProvider: false, 176 | documentFormattingProvider: true, 177 | documentRangeFormattingProvider: true, 178 | %% documentOnTypeFormattingProvider: false, 179 | renameProvider: true, 180 | % documentLinkProvider: false, 181 | % colorProvider: true, 182 | foldingRangeProvider: false, 183 | % [TODO] 184 | % executeCommandProvider: _{commands: ["query", "assert"]}, 185 | semanticTokensProvider: _{legend: _{tokenTypes: TokenTypes, 186 | tokenModifiers: TokenModifiers}, 187 | range: true, 188 | % [TODO] implement deltas 189 | full: _{delta: false}}, 190 | workspace: _{workspaceFolders: _{supported: true, 191 | changeNotifications: true}}}) :- 192 | token_types(TokenTypes), 193 | token_modifiers(TokenModifiers). 194 | 195 | :- dynamic loaded_source/1. 196 | 197 | % messages (with a response) 198 | handle_msg("initialize", Msg, 199 | _{id: Id, result: _{capabilities: ServerCapabilities}}) :- 200 | _{id: Id, params: Params} :< Msg, !, 201 | ( Params.rootUri \== null 202 | -> ( url_path(Params.rootUri, RootPath), 203 | directory_source_files(RootPath, Files, [recursive(true)]), 204 | maplist([F]>>assert(loaded_source(F)), Files) ) 205 | ; true ), 206 | server_capabilities(ServerCapabilities). 207 | handle_msg("shutdown", Msg, _{id: Id, result: []}) :- 208 | _{id: Id} :< Msg, 209 | debug(server, "received shutdown message", []), 210 | asserta(shutdown_request_received). 211 | handle_msg("exit", _Msg, false) :- 212 | debug(server, "received exit, shutting down", []), 213 | asserta(exit_request_received), 214 | ( shutdown_request_received 215 | -> debug(server, "Post-shutdown exit, okay", []) 216 | ; debug(server, "No shutdown, unexpected exit", []), 217 | halt(1) ). 218 | handle_msg("textDocument/hover", Msg, _{id: Id, result: Response}) :- 219 | _{params: _{position: _{character: Char0, line: Line0}, 220 | textDocument: _{uri: Doc}}, id: Id} :< Msg, 221 | url_path(Doc, Path), 222 | Line1 is Line0 + 1, 223 | ( help_at_position(Path, Line1, Char0, Help) 224 | -> Response = _{contents: _{kind: plaintext, value: Help}} 225 | ; Response = null ). 226 | handle_msg("textDocument/documentSymbol", Msg, _{id: Id, result: Symbols}) :- 227 | _{id: Id, params: _{textDocument: _{uri: Doc}}} :< Msg, 228 | url_path(Doc, Path), !, 229 | xref_source(Path), 230 | findall( 231 | Symbol, 232 | ( xref_defined(Path, Goal, local(Line)), 233 | succ(Line, NextLine), 234 | succ(Line0, Line), 235 | functor(Goal, Name, Arity), 236 | format(string(GoalName), "~w/~w", [Name, Arity]), 237 | Symbol = _{name: GoalName, 238 | kind: 12, % function 239 | location: 240 | _{uri: Doc, 241 | range: _{start: _{line: Line0, character: 1}, 242 | end: _{line: NextLine, character: 0}}}} 243 | ), 244 | Symbols). 245 | handle_msg("textDocument/definition", Msg, _{id: Id, result: Location}) :- 246 | _{id: Id, params: Params} :< Msg, 247 | _{textDocument: _{uri: Doc}, 248 | position: _{line: Line0, character: Char0}} :< Params, 249 | url_path(Doc, Path), 250 | succ(Line0, Line1), 251 | clause_in_file_at_position(Name/Arity, Path, line_char(Line1, Char0)), 252 | defined_at(Path, Name/Arity, Location). 253 | handle_msg("textDocument/definition", Msg, _{id: Msg.id, result: null}) :- !. 254 | handle_msg("textDocument/references", Msg, _{id: Id, result: Locations}) :- 255 | _{id: Id, params: Params} :< Msg, 256 | _{textDocument: _{uri: Uri}, 257 | position: _{line: Line0, character: Char0}} :< Params, 258 | url_path(Uri, Path), 259 | succ(Line0, Line1), 260 | clause_in_file_at_position(Clause, Path, line_char(Line1, Char0)), 261 | findall( 262 | Location, 263 | ( loaded_source(Doc), 264 | url_path(DocUri, Doc), 265 | called_at(Doc, Clause, Locs0), 266 | % handle the case where Caller = imported(Path)? 267 | maplist([D0, D]>>put_dict(uri, D0, DocUri, D), Locs0, Locs1), 268 | member(Location, Locs1) 269 | ), 270 | Locations0), !, 271 | ordered_locations(Locations0, Locations). 272 | handle_msg("textDocument/references", Msg, _{id: Msg.id, result: null}) :- !. 273 | handle_msg("textDocument/completion", Msg, _{id: Id, result: Completions}) :- 274 | _{id: Id, params: Params} :< Msg, 275 | _{textDocument: _{uri: Uri}, 276 | position: _{line: Line0, character: Char0}} :< Params, 277 | url_path(Uri, Path), 278 | succ(Line0, Line1), 279 | completions_at(Path, line_char(Line1, Char0), Completions). 280 | handle_msg("textDocument/formatting", Msg, _{id: Id, result: Edits}) :- 281 | _{id: Id, params: Params} :< Msg, 282 | _{textDocument: _{uri: Uri}} :< Params, 283 | url_path(Uri, Path), 284 | file_format_edits(Path, Edits). 285 | handle_msg("textDocument/rangeFormatting", Msg, _{id: Id, result: Edits}) :- 286 | _{id: Id, params: Params} :< Msg, 287 | _{textDocument: _{uri: Uri}, range: Range} :< Params, 288 | url_path(Uri, Path), 289 | file_format_edits(Path, Edits0), 290 | include(edit_in_range(Range), Edits0, Edits). 291 | handle_msg("textDocument/documentHighlight", Msg, _{id: Id, result: Locations}) :- 292 | _{id: Id, params: Params} :< Msg, 293 | _{textDocument: _{uri: Uri}, 294 | position: _{line: Line0, character: Char0}} :< Params, 295 | url_path(Uri, Path), 296 | succ(Line0, Line1), 297 | highlights_at_position(Path, line_char(Line1, Char0), Locations), !. 298 | handle_msg("textDocument/documentHighlight", Msg, _{id: Id, result: null}) :- 299 | _{id: Id} :< Msg. 300 | handle_msg("textDocument/rename", Msg, _{id: Id, result: Result}) :- 301 | _{id: Id, params: Params} :< Msg, 302 | _{textDocument: _{uri: Uri}, 303 | position: _{line: Line0, character: Char0}, 304 | newName: NewName} :< Params, 305 | url_path(Uri, Path), 306 | succ(Line0, Line1), 307 | % highlights_at_position gives us the location & span of the variables 308 | % using the 4-arity version instead of 3 so we can specify it should only match a variable 309 | lsp_highlights:highlights_at_position(Path, line_char(Line1, Char0), '$var'(_), 310 | Positions), 311 | maplist([P0, P1]>>put_dict(newText, P0, NewName, P1), Positions, Edits), 312 | atom_string(AUri, Uri), % dict key must be an atom 313 | dict_create(Changes, _, [AUri=Edits]), 314 | Result = _{changes: Changes}. 315 | handle_msg("textDocument/rename", Msg, _{id: Id, error: _{message: "Nothing that can be renamed here.", 316 | code: -32602}}) :- 317 | _{id: Id} :< Msg. 318 | handle_msg("textDocument/semanticTokens", Msg, Response) :- 319 | handle_msg("textDocument/semanticTokens/full", Msg, Response). 320 | handle_msg("textDocument/semanticTokens/full", Msg, 321 | _{id: Id, result: _{data: Highlights}}) :- 322 | _{id: Id, params: Params} :< Msg, 323 | _{textDocument: _{uri: Uri}} :< Params, 324 | url_path(Uri, Path), !, 325 | xref_source(Path), 326 | file_colours(Path, Highlights). 327 | handle_msg("textDocument/semanticTokens/range", Msg, 328 | _{id: Id, result: _{data: Highlights}}) :- 329 | _{id: Id, params: Params} :< Msg, 330 | _{textDocument: _{uri: Uri}, range: Range} :< Params, 331 | _{start: _{line: StartLine0, character: StartChar}, 332 | end: _{line: EndLine0, character: EndChar}} :< Range, 333 | url_path(Uri, Path), !, 334 | succ(StartLine0, StartLine), succ(EndLine0, EndLine), 335 | xref_source(Path), 336 | file_range_colours(Path, 337 | line_char(StartLine, StartChar), 338 | line_char(EndLine, EndChar), 339 | Highlights). 340 | % notifications (no response) 341 | handle_msg("textDocument/didOpen", Msg, Resp) :- 342 | _{params: _{textDocument: TextDoc}} :< Msg, 343 | _{uri: FileUri} :< TextDoc, 344 | url_path(FileUri, Path), 345 | ( loaded_source(Path) ; assertz(loaded_source(Path)) ), 346 | check_errors_resp(FileUri, Resp). 347 | handle_msg("textDocument/didChange", Msg, false) :- 348 | _{params: _{textDocument: TextDoc, 349 | contentChanges: Changes}} :< Msg, 350 | _{uri: Uri} :< TextDoc, 351 | url_path(Uri, Path), 352 | handle_doc_changes(Path, Changes). 353 | handle_msg("textDocument/didSave", Msg, Resp) :- 354 | _{params: Params} :< Msg, 355 | check_errors_resp(Params.textDocument.uri, Resp). 356 | handle_msg("textDocument/didClose", Msg, false) :- 357 | _{params: _{textDocument: TextDoc}} :< Msg, 358 | _{uri: FileUri} :< TextDoc, 359 | url_path(FileUri, Path), 360 | retractall(loaded_source(Path)). 361 | handle_msg("initialized", Msg, false) :- 362 | debug(server, "initialized ~w", [Msg]). 363 | handle_msg("$/cancelRequest", _Msg, false). 364 | % wildcard 365 | handle_msg(_, Msg, _{id: Id, error: _{code: -32603, message: "Unimplemented"}}) :- 366 | _{id: Id} :< Msg, !, 367 | debug(server, "unknown message ~w", [Msg]). 368 | handle_msg(_, Msg, false) :- 369 | debug(server, "unknown notification ~w", [Msg]). 370 | 371 | check_errors_resp(FileUri, _{method: "textDocument/publishDiagnostics", 372 | params: _{uri: FileUri, diagnostics: Errors}}) :- 373 | url_path(FileUri, Path), 374 | check_errors(Path, Errors). 375 | check_errors_resp(_, false) :- 376 | debug(server, "Failed checking errors", []). 377 | 378 | edit_in_range(Range, Edit) :- 379 | _{start: _{line: RStartLine, character: RStartChar}, 380 | end: _{line: REndLine, character: REndChar}} :< Range, 381 | _{start: _{line: EStartLine, character: EStartChar}, 382 | end: _{line: EEndLine, character: EEndChar}} :< Edit.range, 383 | RStartLine =< EStartLine, REndLine >= EEndLine, 384 | ( RStartLine == EStartLine 385 | -> RStartChar =< EStartChar 386 | % do we care to restrict the *end* of the edit? 387 | ; ( REndLine == EEndLine 388 | -> REndChar >= EEndChar 389 | ; true ) ). 390 | 391 | %! ordered_locations(+Locations:list(dict), +Locations:list(dict)) is det. 392 | % 393 | % Sort range dictionaries into ascending order of start line. 394 | ordered_locations(Locations, OrderedLocations) :- 395 | maplist([D, SL-D]>>( get_dict(range, D, Range), 396 | get_dict(start, Range, Start), 397 | get_dict(line, Start, SL) ), 398 | Locations, 399 | Locs1), 400 | sort(1, @=<, Locs1, Locs2), 401 | maplist([_-D, D]>>true, Locs2, OrderedLocations). 402 | -------------------------------------------------------------------------------- /prolog/lsp_utils.pl: -------------------------------------------------------------------------------- 1 | :- module(lsp_utils, [called_at/3, 2 | defined_at/3, 3 | name_callable/2, 4 | relative_ref_location/4, 5 | help_at_position/4, 6 | clause_in_file_at_position/3, 7 | clause_variable_positions/3, 8 | seek_to_line/2, 9 | linechar_offset/3, 10 | url_path/2 11 | ]). 12 | /** LSP Utils 13 | 14 | Module with a bunch of helper predicates for looking through prolog 15 | source and stuff. 16 | 17 | @author James Cash 18 | */ 19 | 20 | :- use_module(library(apply_macros)). 21 | :- use_module(library(apply), [maplist/3, exclude/3]). 22 | :- use_module(library(prolog_xref)). 23 | :- use_module(library(prolog_source), [read_source_term_at_location/3]). 24 | :- use_module(library(help), [help_html/3, help_objects/3]). 25 | :- use_module(library(lynx/html_text), [html_text/1]). 26 | :- use_module(library(solution_sequences), [distinct/2]). 27 | :- use_module(library(lists), [append/3, member/2, selectchk/4]). 28 | :- use_module(library(sgml), [load_html/3]). 29 | :- use_module(library(yall)). 30 | 31 | :- include('_lsp_path_add.pl'). 32 | 33 | :- use_module(lsp(lsp_reading_source), [ file_lines_start_end/2, 34 | read_term_positions/2, 35 | read_term_positions/4, 36 | find_in_term_with_positions/5, 37 | position_to_match/3, 38 | file_offset_line_position/4 ]). 39 | 40 | :- if(current_predicate(xref_called/5)). 41 | %! called_at(+Path:atom, +Clause:term, -Locations:list) is det. 42 | % Find the callers and locations of the goal =Clause=, starting from 43 | % the file =Path=. =Locations= will be a list of all the callers and 44 | % locations that the =Clause= is called from as LSP-formatted dicts. 45 | called_at(Path, Clause, Locations) :- 46 | setof(L, Path^Clause^Locs^( 47 | called_at_(Path, Clause, Locs), 48 | member(L, Locs) 49 | ), 50 | Locations), !. 51 | called_at(Path, Clause, Locations) :- 52 | name_callable(Clause, Callable), 53 | xref_source(Path), 54 | xref_called(Path, Callable, _By, _, CallerLine), 55 | % we couldn't find the definition, but we know it's in that form, so give that at least 56 | succ(CallerLine0, CallerLine), 57 | Locations = [_{range: _{start: _{line: CallerLine0, character: 0}, 58 | end: _{line: CallerLine, character: 0}}}]. 59 | 60 | called_at_(Path, Clause, Locations) :- 61 | name_callable(Clause, Callable), 62 | xref_source(Path), 63 | xref_called(Path, Callable, _By, _, CallerLine), 64 | file_lines_start_end(Path, LineCharRange), 65 | file_offset_line_position(LineCharRange, Offset, CallerLine, 0), 66 | read_term_positions(Path, Offset, Offset, TermInfos), 67 | Clause = FuncName/Arity, 68 | find_occurences_of_callable(Path, FuncName, Arity, TermInfos, Matches, []), 69 | maplist(position_to_match(LineCharRange), Matches, Locations). 70 | called_at_(Path, Clause, Locations) :- 71 | xref_source(Path), 72 | Clause = FuncName/Arity, 73 | DcgArity is Arity + 2, 74 | DcgClause = FuncName/DcgArity, 75 | name_callable(DcgClause, DcgCallable), 76 | xref_defined(Path, DcgCallable, dcg), 77 | name_callable(DcgClause, DcgCallable), 78 | xref_called(Path, DcgCallable, _By, _, CallerLine), 79 | file_lines_start_end(Path, LineCharRange), 80 | file_offset_line_position(LineCharRange, Offset, CallerLine, 0), 81 | read_term_positions(Path, Offset, Offset, TermInfos), 82 | find_occurences_of_callable(Path, FuncName, DcgArity, TermInfos, Matches, Tail0), 83 | % also look for original arity in a dcg context 84 | % TODO: modify this to check that it's inside a DCG if it has this 85 | % arity...but not in braces? 86 | find_occurences_of_callable(Path, FuncName, Arity, TermInfos, Tail0, []), 87 | maplist(position_to_match(LineCharRange), Matches, Locations). 88 | :- else. 89 | called_at(Path, Callable, By, Ref) :- 90 | xref_called(Path, Callable, By), 91 | xref_defined(Path, By, Ref). 92 | :- endif. 93 | 94 | find_occurences_of_callable(_, _, _, [], Tail, Tail). 95 | find_occurences_of_callable(Path, FuncName, Arity, [TermInfo|TermInfos], Matches, Tail) :- 96 | FindState = in_meta(false), 97 | find_in_term_with_positions(term_matches_callable(FindState, Path, FuncName, Arity), 98 | TermInfo.term, TermInfo.subterm, Matches, Tail0), 99 | find_occurences_of_callable(Path, FuncName, Arity, TermInfos, Tail0, Tail). 100 | 101 | term_matches_callable(FindState, Path, FuncName, Arity, Term, Position) :- 102 | arg(1, Position, Start), 103 | arg(2, Position, End), 104 | ( arg(1, FindState, in_meta(_, MStart, MEnd)), 105 | once( Start > MEnd ; End < MStart ) 106 | -> nb_setarg(1, FindState, false) 107 | ; true ), 108 | term_matches_callable_(FindState, Path, FuncName, Arity, Term, Position). 109 | 110 | term_matches_callable_(_, _, FuncName, Arity, Term, _) :- 111 | nonvar(Term), Term = FuncName/Arity. 112 | term_matches_callable_(_, _, FuncName, Arity, Term, _) :- 113 | nonvar(Term), 114 | functor(T, FuncName, Arity), 115 | Term = T, !. 116 | term_matches_callable_(State, _, FuncName, Arity, Term, _) :- 117 | nonvar(Term), 118 | % TODO check the argument 119 | arg(1, State, in_meta(N, _, _)), 120 | MArity is Arity - N, 121 | functor(T, FuncName, MArity), 122 | Term = T, !. 123 | term_matches_callable_(State, Path, _, _, Term, Position) :- 124 | nonvar(Term), compound(Term), 125 | compound_name_arity(Term, ThisName, ThisArity), 126 | name_callable(ThisName/ThisArity, Callable), 127 | xref_meta(Path, Callable, Called), 128 | member(E, Called), nonvar(E), E = _+N, integer(N), 129 | arg(1, Position, Start), 130 | arg(2, Position, End), 131 | nb_setarg(1, State, in_meta(N, Start, End)), 132 | fail. 133 | 134 | %! url_path(?FileUrl:atom, ?Path:atom) is det. 135 | % 136 | % Convert between file:// url and path 137 | url_path(Url, Path) :- 138 | current_prolog_flag(windows, true), 139 | % on windows, in neovim at least, textDocument URI looks like 140 | % "file:///C:/foo/bar/baz.pl"; we need to strip off another 141 | % leading slash to get a valid path 142 | atom_concat('file:///', Path, Url), !. 143 | url_path(Url, Path) :- 144 | atom_concat('file://', Path, Url). 145 | 146 | defined_at(Path, Name/Arity, Location) :- 147 | name_callable(Name/Arity, Callable), 148 | xref_source(Path), 149 | xref_defined(Path, Callable, Ref), 150 | url_path(Doc, Path), 151 | relative_ref_location(Doc, Callable, Ref, Location). 152 | defined_at(Path, Name/Arity, Location) :- 153 | % maybe it's a DCG? 154 | DcgArity is Arity + 2, 155 | name_callable(Name/DcgArity, Callable), 156 | xref_source(Path), 157 | xref_defined(Path, Callable, Ref), 158 | url_path(Doc, Path), 159 | relative_ref_location(Doc, Callable, Ref, Location). 160 | 161 | 162 | find_subclause(Stream, Subclause, CallerLine, Locations) :- 163 | read_source_term_at_location(Stream, Term, [line(CallerLine), 164 | subterm_positions(Poses)]), 165 | findall(Offset, distinct(Offset, find_clause(Term, Offset, Poses, Subclause)), 166 | Offsets), 167 | collapse_adjacent(Offsets, StartOffsets), 168 | maplist(offset_line_char(Stream), StartOffsets, Locations). 169 | 170 | offset_line_char(Stream, Offset, position(Line, Char)) :- 171 | % seek(Stream, 0, bof, _), 172 | % for some reason, seek/4 isn't zeroing stream line position 173 | set_stream_position(Stream, '$stream_position'(0, 0, 0, 0)), 174 | setup_call_cleanup( 175 | open_null_stream(NullStream), 176 | copy_stream_data(Stream, NullStream, Offset), 177 | close(NullStream) 178 | ), 179 | stream_property(Stream, position(Pos)), 180 | stream_position_data(line_count, Pos, Line), 181 | stream_position_data(line_position, Pos, Char). 182 | 183 | collapse_adjacent([X|Rst], [X|CRst]) :- 184 | collapse_adjacent(X, Rst, CRst). 185 | collapse_adjacent(X, [Y|Rst], CRst) :- 186 | succ(X, Y), !, 187 | collapse_adjacent(Y, Rst, CRst). 188 | collapse_adjacent(_, [X|Rst], [X|CRst]) :- !, 189 | collapse_adjacent(X, Rst, CRst). 190 | collapse_adjacent(_, [], []). 191 | 192 | 193 | %! name_callable(?Name:functor, ?Callable:term) is det. 194 | % True when, if Name = Func/Arity, Callable = Func(_, _, ...) with 195 | % =Arity= args. 196 | name_callable(Name/0, Name) :- atom(Name), !. 197 | name_callable(Name/Arity, Callable) :- 198 | length(FakeArgs, Arity), 199 | Callable =.. [Name|FakeArgs], !. 200 | 201 | %! relative_ref_location(+Path:atom, +Goal:term, +Position:position(int, int), -Location:dict) is semidet. 202 | % Given =Goal= found in =Path= and position =Position= (from 203 | % called_at/3), =Location= is a dictionary suitable for sending as an 204 | % LSP response indicating the position in a file of =Goal=. 205 | relative_ref_location(Here, _, position(Line0, Char1), 206 | _{uri: Here, range: _{start: _{line: Line0, character: Char1}, 207 | end: _{line: Line1, character: 0}}}) :- 208 | !, succ(Line0, Line1). 209 | relative_ref_location(Here, _, local(Line1), 210 | _{uri: Here, range: _{start: _{line: Line0, character: 1}, 211 | end: _{line: NextLine, character: 0}}}) :- 212 | !, succ(Line0, Line1), succ(Line1, NextLine). 213 | relative_ref_location(_, Goal, imported(Path), Location) :- 214 | url_path(ThereUri, Path), 215 | xref_source(Path), 216 | xref_defined(Path, Goal, Loc), 217 | relative_ref_location(ThereUri, Goal, Loc, Location). 218 | 219 | %! help_at_position(+Path:atom, +Line:integer, +Char:integer, -Help:string) is det. 220 | % 221 | % =Help= is the documentation for the term under the cursor at line 222 | % =Line=, character =Char= in the file =Path=. 223 | help_at_position(Path, Line1, Char0, S) :- 224 | clause_in_file_at_position(Clause, Path, line_char(Line1, Char0)), 225 | predicate_help(Path, Clause, S0), 226 | format_help(S0, S). 227 | 228 | %! format_help(+Help0, -Help1) is det. 229 | % 230 | % Reformat help string, so the first line is the signature of the predicate. 231 | format_help(HelpFull, Help) :- 232 | split_string(HelpFull, "\n", " ", Lines0), 233 | exclude([Line]>>string_concat("Availability: ", _, Line), 234 | Lines0, Lines1), 235 | exclude(=(""), Lines1, Lines2), 236 | Lines2 = [HelpShort|_], 237 | split_string(HelpFull, "\n", " ", HelpLines), 238 | selectchk(HelpShort, HelpLines, "", HelpLines0), 239 | append([HelpShort], HelpLines0, HelpLines1), 240 | atomic_list_concat(HelpLines1, "\n", Help). 241 | 242 | predicate_help(_, Pred, Help) :- 243 | nonvar(Pred), 244 | help_objects(Pred, exact, Matches), !, 245 | catch(help_html(Matches, exact-exact, HtmlDoc), _, fail), 246 | setup_call_cleanup(open_string(HtmlDoc, In), 247 | load_html(stream(In), Dom, []), 248 | close(In)), 249 | with_output_to(string(Help), html_text(Dom)). 250 | predicate_help(HerePath, Pred, Help) :- 251 | xref_source(HerePath), 252 | name_callable(Pred, Callable), 253 | xref_defined(HerePath, Callable, Loc), 254 | location_path(HerePath, Loc, Path), 255 | once(xref_comment(Path, Callable, Summary, Comment)), 256 | pldoc_process:parse_comment(Comment, Path:0, Parsed), 257 | memberchk(mode(Signature, Mode), Parsed), 258 | memberchk(predicate(_, Summary, _), Parsed), 259 | format(string(Help), " ~w is ~w.~n~n~w", [Signature, Mode, Summary]). 260 | predicate_help(_, Pred/_Arity, Help) :- 261 | help_objects(Pred, dwim, Matches), !, 262 | catch(help_html(Matches, dwim-Pred, HtmlDoc), _, fail), 263 | setup_call_cleanup(open_string(HtmlDoc, In), 264 | load_html(stream(In), Dom, []), 265 | close(In)), 266 | with_output_to(string(Help), html_text(Dom)). 267 | 268 | location_path(HerePath, local(_), HerePath). 269 | location_path(_, imported(Path), Path). 270 | 271 | linechar_offset(Stream, line_char(Line1, Char0), Offset) :- 272 | seek(Stream, 0, bof, _), 273 | seek_to_line(Stream, Line1), 274 | seek(Stream, Char0, current, Offset). 275 | 276 | seek_to_line(Stream, N) :- 277 | N > 1, !, 278 | skip(Stream, 0'\n), 279 | NN is N - 1, 280 | seek_to_line(Stream, NN). 281 | seek_to_line(_, _). 282 | 283 | clause_variable_positions(Path, Line, Variables) :- 284 | file_lines_start_end(Path, LineCharRange), 285 | read_term_positions(Path, TermsWithPositions), 286 | % find the top-level term that the offset falls within 287 | file_offset_line_position(LineCharRange, Offset, Line, 0), 288 | member(TermInfo, TermsWithPositions), 289 | SubTermPoses = TermInfo.subterm, 290 | arg(1, SubTermPoses, TermFrom), 291 | arg(2, SubTermPoses, TermTo), 292 | between(TermFrom, TermTo, Offset), !, 293 | find_in_term_with_positions( 294 | [X, _]>>( \+ \+ ( X = '$var'(Name), ground(Name) ) ), 295 | TermInfo.term, 296 | TermInfo.subterm, 297 | VariablesPositions, [] 298 | ), 299 | findall( 300 | VarName-Locations, 301 | group_by( 302 | VarName, 303 | Location, 304 | ( member(found_at('$var'(VarName), Location0-_), VariablesPositions), 305 | file_offset_line_position(LineCharRange, Location0, L1, C), 306 | succ(L0, L1), 307 | Location = position(L0, C) 308 | ), 309 | Locations 310 | ), 311 | Variables). 312 | 313 | clause_in_file_at_position(Clause, Path, Position) :- 314 | xref_source(Path), 315 | findall(Op, xref_op(Path, Op), Ops), 316 | setup_call_cleanup( 317 | open(Path, read, Stream, []), 318 | clause_at_position(Stream, Ops, Clause, Position), 319 | close(Stream) 320 | ). 321 | 322 | clause_at_position(Stream, Ops, Clause, Start) :- 323 | linechar_offset(Stream, Start, Offset), !, 324 | clause_at_position(Stream, Ops, Clause, Start, Offset). 325 | clause_at_position(Stream, Ops, Clause, line_char(Line1, Char), Here) :- 326 | read_source_term_at_location(Stream, Terms, [line(Line1), 327 | subterm_positions(SubPos), 328 | operators(Ops), 329 | error(Error)]), 330 | extract_clause_at_position(Stream, Ops, Terms, line_char(Line1, Char), Here, 331 | SubPos, Error, Clause). 332 | 333 | extract_clause_at_position(Stream, Ops, _, line_char(Line1, Char), Here, _, 334 | Error, Clause) :- 335 | nonvar(Error), !, Line1 > 1, 336 | LineBack is Line1 - 1, 337 | clause_at_position(Stream, Ops, Clause, line_char(LineBack, Char), Here). 338 | extract_clause_at_position(_, _, Terms, _, Here, SubPos, _, Clause) :- 339 | once(find_clause(Terms, Here, SubPos, Clause)). 340 | 341 | %! find_clause(+Term:term, ?Offset:int, +Position:position, ?Subclause) is nondet. 342 | % True when =Subclause= is a subclause of =Term= at offset =Offset= 343 | % and =Position= is the term positions for =Term= as given by 344 | % read_term/3 with =subterm_positions(Position)=. 345 | find_clause(Term, Offset, F-T, Clause) :- 346 | between(F, T, Offset), 347 | ground(Term), Clause = Term/0. 348 | find_clause(Term, Offset, term_position(_, _, FF, FT, _), Name/Arity) :- 349 | between(FF, FT, Offset), 350 | functor(Term, Name, Arity). 351 | find_clause(Term, Offset, term_position(F, T, _, _, SubPoses), Clause) :- 352 | between(F, T, Offset), 353 | Term =.. [_|SubTerms], 354 | find_containing_term(Offset, SubTerms, SubPoses, SubTerm, SubPos), 355 | find_clause(SubTerm, Offset, SubPos, Clause). 356 | find_clause(Term, Offset, parentheses_term_position(F, T, SubPoses), Clause) :- 357 | between(F, T, Offset), 358 | find_clause(Term, Offset, SubPoses, Clause). 359 | find_clause({SubTerm}, Offset, brace_term_position(F, T, SubPos), Clause) :- 360 | between(F, T, Offset), 361 | find_clause(SubTerm, Offset, SubPos, Clause). 362 | 363 | find_containing_term(Offset, [Term|_], [F-T|_], Term, F-T) :- 364 | between(F, T, Offset). 365 | find_containing_term(Offset, [Term|_], [P|_], Term, P) :- 366 | P = term_position(F, T, _, _, _), 367 | between(F, T, Offset), !. 368 | find_containing_term(Offset, [Term|_], [PP|_], Term, P) :- 369 | PP = parentheses_term_position(F, T, P), 370 | between(F, T, Offset), !. 371 | find_containing_term(Offset, [BTerm|_], [BP|_], Term, P) :- 372 | BP = brace_term_position(F, T, P), 373 | {Term} = BTerm, 374 | between(F, T, Offset). 375 | find_containing_term(Offset, [Terms|_], [LP|_], Term, P) :- 376 | LP = list_position(_F, _T, Ps, _), 377 | find_containing_term(Offset, Terms, Ps, Term, P). 378 | find_containing_term(Offset, [Dict|_], [DP|_], Term, P) :- 379 | DP = dict_position(_, _, _, _, Ps), 380 | member(key_value_position(_F, _T, _SepF, _SepT, Key, _KeyPos, ValuePos), 381 | Ps), 382 | get_dict(Key, Dict, Value), 383 | find_containing_term(Offset, [Value], [ValuePos], Term, P). 384 | find_containing_term(Offset, [_|Ts], [_|Ps], T, P) :- 385 | find_containing_term(Offset, Ts, Ps, T, P). 386 | -------------------------------------------------------------------------------- /run_tests.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -euo pipefail 4 | 5 | declare -a load_test_files 6 | for f in test/*.plt; do 7 | load_test_files+=( "-l" ) 8 | load_test_files+=( "${f}" ) 9 | done 10 | exec swipl --quiet ${load_test_files[@]} -g plunit:run_tests -g nl -t halt 11 | -------------------------------------------------------------------------------- /scripts/make_release.pl: -------------------------------------------------------------------------------- 1 | :- module(make_release, []). 2 | 3 | :- use_module(library(readutil), [read_file_to_terms/3, 4 | read_line_to_string/2]). 5 | 6 | :- initialization(main, main). 7 | 8 | increment_version(major, [Major0, _Minor, _Patch], [Major1, 0, 0]) :- !, 9 | succ(Major0, Major1). 10 | increment_version(minor, [Major, Minor0, _Patch], [Major, Minor1, 0]) :- !, 11 | succ(Minor0, Minor1). 12 | increment_version(patch, [Major, Minor, Patch0], [Major, Minor, Patch1]) :- !, 13 | succ(Patch0, Patch1). 14 | 15 | update_pack_version(ReleaseType, NewVersion) :- 16 | read_file_to_terms('pack.pl', PackTerms, []), 17 | memberchk(version(OldVersion), PackTerms), 18 | atomic_list_concat([MajorS, MinorS, PatchS], '.', OldVersion), 19 | maplist(atom_number, [MajorS, MinorS, PatchS], VersionNums0), 20 | increment_version(ReleaseType, VersionNums0, VersionNums1), 21 | atomic_list_concat(VersionNums1, '.', NewVersion), 22 | once(select(version(OldVersion), PackTerms, version(NewVersion), NewPackTerms)), 23 | setup_call_cleanup(open('pack.pl', write, S, []), 24 | forall(member(T, NewPackTerms), 25 | write_term(S, T, [fullstop(true), 26 | nl(true), 27 | quoted(true), 28 | spacing(next_argument) 29 | ])), 30 | close(S)). 31 | 32 | git_commit_and_tag(NewVersion) :- 33 | shell('git add pack.pl'), 34 | shell('git commit -m "Bump version"'), 35 | format(atom(TagCmd), "git tag v~w", [NewVersion]), 36 | shell(TagCmd), 37 | format(atom(PushCmd), 'git push origin master v~w', [NewVersion]), 38 | shell(PushCmd). 39 | 40 | register_new_pack(NewVersion) :- 41 | ( pack_remove(lsp_server) -> true ; true ), 42 | format(atom(Url), 43 | "https://github.com/jamesnvc/lsp_server/archive/refs/tags/v~w.zip", 44 | [NewVersion]), 45 | pack_install(lsp_server, [url(Url), interactive(false)]). 46 | 47 | main(Args) :- 48 | ( Args = [ReleaseType], increment_version(ReleaseType, [0, 0, 0], _) 49 | -> true 50 | ; ( format(user_error, "Usage: make_release.pl [major|minor|patch]~n", []), 51 | halt(1) ) ), 52 | ( stream_property(user_input, tty(true)) 53 | -> format("Make new release? [y/n]: ", []), 54 | read_line_to_string(user_input, Input), 55 | Input == "y" 56 | ; true ), 57 | update_pack_version(ReleaseType, NewVersion), 58 | format("Bumping to ~w~n", [NewVersion]), 59 | git_commit_and_tag(NewVersion), 60 | register_new_pack(NewVersion). 61 | -------------------------------------------------------------------------------- /test/changes.plt: -------------------------------------------------------------------------------- 1 | :- module(changes_t, []). 2 | 3 | :- use_module(library(plunit)). 4 | 5 | :- include('../prolog/_lsp_path_add.pl'). 6 | 7 | :- use_module(lsp(lsp_changes)). 8 | 9 | :- begin_tests(changes). 10 | 11 | test('Replacing range 1', [ true( Changed =@= `foo\nbart\nbaz\n` )]) :- 12 | lsp_changes:replace_codes_range(`foo\nbar\nbaz\n`, 1, 3, 1, 3, `t`, Changed). 13 | 14 | test('Replacing range 2', [ true( Changed =@= `foo\nbat\nbaz\n` )]) :- 15 | lsp_changes:replace_codes_range(`foo\nbar\nbaz\n`, 1, 2, 1, 3, `t`, Changed). 16 | 17 | test('Replacing range 3', [ true( Changed =@= `for\nrrr\nraz\n` )]) :- 18 | lsp_changes:replace_codes_range(`foo\nbar\nbaz\n`, 0, 2, 2, 1, `r\nrrr\nr`, Changed). 19 | 20 | :- end_tests(changes). 21 | -------------------------------------------------------------------------------- /test/checking.plt: -------------------------------------------------------------------------------- 1 | :- module(checking_t, []). 2 | 3 | :- use_module(library(plunit)). 4 | 5 | :- include('../prolog/_lsp_path_add.pl'). 6 | :- use_module(lsp(lsp_checking)). 7 | 8 | :- use_module(library(filesex), [relative_file_name/3]). 9 | :- use_module(library(readutil), [ read_file_to_string/3 ]). 10 | 11 | :- begin_tests(checking). 12 | 13 | test('Errors for test file', 14 | [ true( Errors =@= [_{message: "Operator expected", 15 | range: _{start: _{line: 6, character: 11}, end: _{line: 7, character: 0}}, 16 | severity: 1, 17 | source: "prolog_xref"}, 18 | _{message: "Singleton variable Gae", 19 | range: _{start: _{line: 1, character: 4}, end: _{line: 1, character: 7}}, 20 | severity: 2, 21 | source: "prolog_xref"}, 22 | _{message: "Singleton variable B", 23 | range: _{start: _{line: 3, character: 9}, end: _{line: 3, character: 10}}, 24 | severity: 2, 25 | source: "prolog_xref"}, 26 | _{message: "Singleton variable Z", 27 | range: _{start: _{line: 4, character: 11}, end: _{line: 4, character: 12}}, 28 | severity: 2, 29 | source: "prolog_xref"}] ) ]) :- 30 | module_property(formatter_t, file(ThisFile)), 31 | relative_file_name(InputFile, ThisFile, './checking_input1.pl'), 32 | check_errors(InputFile, Errors). 33 | 34 | :- end_tests(checking). 35 | -------------------------------------------------------------------------------- /test/checking_input1.pl: -------------------------------------------------------------------------------- 1 | foo(A) :- 2 | Gae, 3 | A = 1, 4 | _{a: B}, 5 | [1,2,3|Z]. 6 | 7 | bar :- aoeu snth. 8 | -------------------------------------------------------------------------------- /test/format_input1.pl: -------------------------------------------------------------------------------- 1 | :- module(format_test, [ stream_position_at_offset/3 ]). 2 | /** LSP Formatter test 3 | 4 | Module for formatting Prolog source code 5 | 6 | @author James Cash 7 | 8 | */ 9 | 10 | :- use_module(library(prolog_source)). 11 | :- use_module(library(readutil), [ read_line_to_codes/2, 12 | read_file_to_codes/3]). 13 | :- use_module(library(dcg/basics), [ whites//1]). 14 | 15 | :- dynamic foo/1. 16 | :- dynamic(bar/2). 17 | 18 | stream_position_at_offset(LineCharMap, To, EndPos) :- 19 | A = [1,2,3,4], 20 | _Foo = 'Foo', 21 | _Bar = "Bar", 22 | CharCount = To, 23 | ByteCount = To, % need to check for multibyte... 24 | Neckish = x, 25 | memberchk(Neckish, [':-', '=>', '-->']), 26 | findall(X, ( member(X, A), 27 | 0 is X mod 2 28 | ), 29 | _), 30 | findall(X, 31 | ( member(X, A), 32 | 0 is X mod 2 ), 33 | _ 34 | ), 35 | file_offset_line_position(LineCharMap, To, LineCount, LinePosition), 36 | EndPos = '$stream_position_data'(CharCount, LineCount, LinePosition, ByteCount). 37 | 38 | beep(A), [X] --> 39 | { X = beep }, 40 | { Z = beep, 41 | quux(Z), _W = [ 110,110,32] }, 42 | string_without(`foo`, A). 43 | 44 | emit_reified_(To, term_begin(_, _, Func, _, Parens)) => 45 | ( is_operator(Func) 46 | -> format(To, "~w", [Func]) 47 | ; format(To, "~q", [Func]) ), 48 | ( Parens = true 49 | -> format(To, "(", []) 50 | ; true). 51 | 52 | /* 53 | weird_quasi(Quasi) :- 54 | Quasi = {|html(X)||{{X}}|}. 55 | */ 56 | 57 | burf :- 58 | A = _{ 59 | a: 1, b: [x, "y", 'Z'|Tail], 'C': x{x: 1, b: 2} 60 | }, 61 | write([1,2|_]),write([1,2|[x]]), 62 | B = foo{q: q}, 63 | C = Something{x: y}, 64 | Tail = [gurf], 65 | write(A.b), 66 | Something = aoeu, 67 | format("~q", C), 68 | display(B). 69 | 70 | eval_20(Eq,RetType,Depth,Self,[V|VI],VVO):- \+ is_list(VI),!, 71 | eval_args(Eq,RetType,Depth,Self,VI,VM), 72 | ( VM\==VI -> eval_args(Eq,RetType,Depth,Self,[V|VM],VVO) ; 73 | (eval_args(Eq,RetType,Depth,Self,V,VV), (V\==VV -> eval_args(Eq,RetType,Depth,Self,[VV|VI],VVO) ; VVO = [V|VI]))). 74 | 75 | foo(A) :- 76 | findall(X, ( 77 | member(X, A), 78 | 79 | 0 is X mod 2 80 | ), 81 | _). 82 | 83 | bar(A) :- 84 | findall(X, ( 85 | member(X, A), 86 | 0 is X mod 2 87 | ), 88 | _). 89 | 90 | bas(A) :- 91 | findall(X, ( member(X, A), 92 | 0 is X mod 2 93 | ), 94 | _). 95 | 96 | baz(A) :- 97 | findall(X, 98 | ( member(X, A), 99 | 0 is X mod 2 100 | ), 101 | _). 102 | 103 | whitespace_indentation_for_state(State, Indent) :- 104 | get_dict(state, State, Stack), 105 | aggregate_all(count, 106 | ( member(X, Stack), 107 | memberchk(X, [parens_begin, braces_begin, term_begin(_, _, _)]) ), 108 | ParensCount), 109 | Indent is ParensCount * 2 + _ToplevelIndent. 110 | 111 | %aoeu 112 | hello(A) :- 113 | ( A 114 | -> gurf 115 | ; burf 116 | ). 117 | 118 | send_message(Stream, Msg) :- 119 | put_dict(jsonrpc, Msg, "2.0", VersionedMsg), 120 | atom_json_dict(JsonCodes, VersionedMsg, [as(codes), width(0)]), 121 | phrase(utf8_codes(JsonCodes), UTF8Codes), 122 | length(UTF8Codes, ContentLength), 123 | format(Stream, "Content-Length: ~w\r\n\r\n~s", [ContentLength, JsonCodes]), 124 | flush_output(Stream). 125 | 126 | aoeuoaeuoeau(A) :- 127 | A = [0'a, 0xa, 1e-3, 0.001]. 128 | 129 | expand_subterm_positions(Term, _TermState, term_position(_From, _To, FFrom, FTo, SubPoses), 130 | Expanded, ExTail), functor(Term, ',', _, _) => 131 | % special-case comma terms to be reified as commas 132 | Expanded = [comma(FFrom, FTo)|ExpandedTail0], % aligned 133 | functor(Term, _, Arity, _), % comments 134 | expand_term_subterms_positions(false, Term, Arity, 1, SubPoses, ExpandedTail0, ExTail). 135 | 136 | foo(A, B, C, D, E, F) :- 137 | ( A = 1 138 | -> B = 2 139 | ; ( C = 3 140 | -> D = 4 141 | ; ( A =:= 1 142 | -> E = 5 143 | ; F = 7 ) ) ). 144 | 145 | testing_dict_formatting(A) :- 146 | findall(B, 147 | ( Foo = _{x: 1, 148 | y: 2, 149 | c: 3 150 | }, 151 | Bar = Foo.y, 152 | between(0, Bar, B) 153 | ), 154 | A). 155 | 156 | sthsnthsnth(X) :- 157 | X = ':'(_, _). 158 | 159 | foo_bar(F,B) :- 160 | ( F = f, B = b 161 | ; F = fo, B = ba 162 | ; F = foo, B = bar 163 | ). 164 | 165 | add_use_if_needed__(LastModuleAt, AlreadyImported, Stream, Path, Module, Predicates) :- 166 | repeat, 167 | prolog_read_source_term(Stream, Term, _Ex, [subterm_positions(SubTermPos), 168 | syntax_errors(dec10)]), 169 | once(( Term = (:- use_module(ImpModule)) 170 | ; Term = (:- use_module(ImpModule, _)) 171 | ; Term = (:- ensure_loaded(_)) 172 | ; Term = (:- module(_, _)) 173 | ; Term = end_of_file )), 174 | Term = end_of_file, !, 175 | true. 176 | 177 | add_use_if_needed__(LastModuleAt, AlreadyImported, Stream, Path, Module, Predicates) :- 178 | repeat, 179 | prolog_read_source_term(Stream, Term, _Ex, [subterm_positions(SubTermPos), 180 | syntax_errors(dec10)]), 181 | once(( Term = (:- use_module(ImpModule)) ; 182 | Term = (:- use_module(ImpModule, _)) ; 183 | Term = (:- ensure_loaded(_)) ; 184 | Term = (:- module(_, _)) ; 185 | Term = end_of_file )), 186 | Term = end_of_file, 187 | true. 188 | 189 | % end comment 190 | -------------------------------------------------------------------------------- /test/format_output1.pl: -------------------------------------------------------------------------------- 1 | :- module(format_test, [ stream_position_at_offset/3 ]). 2 | /** LSP Formatter test 3 | 4 | Module for formatting Prolog source code 5 | 6 | @author James Cash 7 | 8 | */ 9 | 10 | :- use_module(library(prolog_source)). 11 | :- use_module(library(readutil), [ read_line_to_codes/2, 12 | read_file_to_codes/3 ]). 13 | :- use_module(library(dcg/basics), [ whites//1 ]). 14 | 15 | :- dynamic foo/1. 16 | :- dynamic(bar/2). 17 | 18 | stream_position_at_offset(LineCharMap, To, EndPos) :- 19 | A = [1, 2, 3, 4], 20 | _Foo = 'Foo', 21 | _Bar = "Bar", 22 | CharCount = To, 23 | ByteCount = To, % need to check for multibyte... 24 | Neckish = x, 25 | memberchk(Neckish, [':-', '=>', '-->']), 26 | findall(X, ( member(X, A), 27 | 0 is X mod 2 28 | ), 29 | _), 30 | findall(X, 31 | ( member(X, A), 32 | 0 is X mod 2 ), 33 | _ 34 | ), 35 | file_offset_line_position(LineCharMap, To, LineCount, LinePosition), 36 | EndPos = '$stream_position_data'(CharCount, LineCount, LinePosition, ByteCount). 37 | 38 | beep(A), [X] --> 39 | { X = beep }, 40 | { Z = beep, 41 | quux(Z), _W = [ 110, 110, 32 ] }, 42 | string_without(`foo`, A). 43 | 44 | emit_reified_(To, term_begin(_, _, Func, _, Parens)) => 45 | ( is_operator(Func) 46 | -> format(To, "~w", [Func]) 47 | ; format(To, "~q", [Func]) ), 48 | ( Parens = true 49 | -> format(To, "(", []) 50 | ; true ). 51 | 52 | /* 53 | weird_quasi(Quasi) :- 54 | Quasi = {|html(X)||{{X}}|}. 55 | */ 56 | 57 | burf :- 58 | A = _{ 59 | a: 1, b: [x, "y", 'Z'|Tail], 'C': x{x: 1, b: 2} 60 | }, 61 | write([1, 2|_]), write([1, 2|[x]]), 62 | B = foo{q: q}, 63 | C = Something{x: y}, 64 | Tail = [gurf], 65 | write(A.b), 66 | Something = aoeu, 67 | format("~q", C), 68 | display(B). 69 | 70 | eval_20(Eq, RetType, Depth, Self, [V|VI], VVO):- \+ is_list(VI), !, 71 | eval_args(Eq, RetType, Depth, Self, VI, VM), 72 | ( VM\==VI -> eval_args(Eq, RetType, Depth, Self, [V|VM], VVO) ; 73 | (eval_args(Eq, RetType, Depth, Self, V, VV), (V\==VV -> eval_args(Eq, RetType, Depth, Self, [VV|VI], VVO) ; VVO = [V|VI])) ). 74 | 75 | foo(A) :- 76 | findall(X, ( 77 | member(X, A), 78 | 79 | 0 is X mod 2 80 | ), 81 | _). 82 | 83 | bar(A) :- 84 | findall(X, ( 85 | member(X, A), 86 | 0 is X mod 2 87 | ), 88 | _). 89 | 90 | bas(A) :- 91 | findall(X, ( member(X, A), 92 | 0 is X mod 2 93 | ), 94 | _). 95 | 96 | baz(A) :- 97 | findall(X, 98 | ( member(X, A), 99 | 0 is X mod 2 100 | ), 101 | _). 102 | 103 | whitespace_indentation_for_state(State, Indent) :- 104 | get_dict(state, State, Stack), 105 | aggregate_all(count, 106 | ( member(X, Stack), 107 | memberchk(X, [parens_begin, braces_begin, term_begin(_, _, _)]) ), 108 | ParensCount), 109 | Indent is ParensCount * 2 + _ToplevelIndent. 110 | 111 | %aoeu 112 | hello(A) :- 113 | ( A 114 | -> gurf 115 | ; burf 116 | ). 117 | 118 | send_message(Stream, Msg) :- 119 | put_dict(jsonrpc, Msg, "2.0", VersionedMsg), 120 | atom_json_dict(JsonCodes, VersionedMsg, [as(codes), width(0)]), 121 | phrase(utf8_codes(JsonCodes), UTF8Codes), 122 | length(UTF8Codes, ContentLength), 123 | format(Stream, "Content-Length: ~w\r\n\r\n~s", [ContentLength, JsonCodes]), 124 | flush_output(Stream). 125 | 126 | aoeuoaeuoeau(A) :- 127 | A = [0'a, 0xa, 1e-3, 0.001]. 128 | 129 | expand_subterm_positions(Term, _TermState, term_position(_From, _To, FFrom, FTo, SubPoses), 130 | Expanded, ExTail), functor(Term, ',', _, _) => 131 | % special-case comma terms to be reified as commas 132 | Expanded = [comma(FFrom, FTo)|ExpandedTail0], % aligned 133 | functor(Term, _, Arity, _), % comments 134 | expand_term_subterms_positions(false, Term, Arity, 1, SubPoses, ExpandedTail0, ExTail). 135 | 136 | foo(A, B, C, D, E, F) :- 137 | ( A = 1 138 | -> B = 2 139 | ; ( C = 3 140 | -> D = 4 141 | ; ( A =:= 1 142 | -> E = 5 143 | ; F = 7 ) ) ). 144 | 145 | testing_dict_formatting(A) :- 146 | findall(B, 147 | ( Foo = _{x: 1, 148 | y: 2, 149 | c: 3 150 | }, 151 | Bar = Foo.y, 152 | between(0, Bar, B) 153 | ), 154 | A). 155 | 156 | sthsnthsnth(X) :- 157 | X = ':'(_, _). 158 | 159 | foo_bar(F, B) :- 160 | ( F = f, B = b 161 | ; F = fo, B = ba 162 | ; F = foo, B = bar 163 | ). 164 | 165 | add_use_if_needed__(LastModuleAt, AlreadyImported, Stream, Path, Module, Predicates) :- 166 | repeat, 167 | prolog_read_source_term(Stream, Term, _Ex, [subterm_positions(SubTermPos), 168 | syntax_errors(dec10)]), 169 | once(( Term = (:- use_module(ImpModule)) 170 | ; Term = (:- use_module(ImpModule, _)) 171 | ; Term = (:- ensure_loaded(_)) 172 | ; Term = (:- module(_, _)) 173 | ; Term = end_of_file )), 174 | Term = end_of_file, !, 175 | true. 176 | 177 | add_use_if_needed__(LastModuleAt, AlreadyImported, Stream, Path, Module, Predicates) :- 178 | repeat, 179 | prolog_read_source_term(Stream, Term, _Ex, [subterm_positions(SubTermPos), 180 | syntax_errors(dec10)]), 181 | once(( Term = (:- use_module(ImpModule)) ; 182 | Term = (:- use_module(ImpModule, _)) ; 183 | Term = (:- ensure_loaded(_)) ; 184 | Term = (:- module(_, _)) ; 185 | Term = end_of_file )), 186 | Term = end_of_file, 187 | true. 188 | 189 | % end comment 190 | -------------------------------------------------------------------------------- /test/formatter.plt: -------------------------------------------------------------------------------- 1 | :- module(formatter_t, []). 2 | 3 | :- use_module(library(plunit)). 4 | 5 | :- include('../prolog/_lsp_path_add.pl'). 6 | :- use_module(lsp(lsp_formatter)). 7 | :- use_module(lsp(lsp_formatter_parser)). 8 | 9 | :- use_module(library(filesex), [relative_file_name/3]). 10 | :- use_module(library(readutil), [ read_file_to_string/3 ]). 11 | 12 | :- begin_tests(formatting). 13 | 14 | test('Formatting example file', 15 | [ true(FormattedText == OutputFileText) ]) :- 16 | module_property(formatter_t, file(ThisFile)), 17 | relative_file_name(InputFile, ThisFile, './format_input1.pl'), 18 | relative_file_name(OutputFile, ThisFile, './format_output1.pl'), 19 | read_file_to_string(OutputFile, OutputFileText, []), 20 | lsp_formatter:file_formatted(InputFile, Formatted), 21 | with_output_to( 22 | string(FormattedText), 23 | lsp_formatter_parser:emit_reified(current_output, Formatted)). 24 | 25 | :- end_tests(formatting). 26 | -------------------------------------------------------------------------------- /test/highlight_input1.pl: -------------------------------------------------------------------------------- 1 | :- module(format_test, [ stream_position_at_offset/3 ]). 2 | /** LSP Formatter test 3 | 4 | Module for formatting Prolog source code 5 | 6 | @author James Cash 7 | 8 | */ 9 | 10 | :- use_module(library(prolog_source)). 11 | :- use_module(library(readutil), [ read_line_to_codes/2, 12 | read_file_to_codes/3]). 13 | :- use_module(library(dcg/basics), [ whites//1]). 14 | 15 | :- dynamic foo/1. 16 | :- dynamic(bar/2). 17 | 18 | stream_position_at_offset(LineCharMap, To, EndPos) :- 19 | A = [1,2,3,4], 20 | _Foo = 'Foo', 21 | _Bar = "Bar", 22 | CharCount = To, 23 | ByteCount = To, % need to check for multibyte... 24 | Neckish = x, 25 | memberchk(Neckish, [':-', '=>', '-->']), 26 | findall(X, ( member(X, A), 27 | 0 is X mod 2 28 | ), 29 | _), 30 | findall(X, 31 | ( member(X, A), 32 | 0 is X mod 2 ), 33 | _), 34 | file_offset_line_position(LineCharMap, To, LineCount, LinePosition), 35 | EndPos = '$stream_position_data'(CharCount, LineCount, LinePosition, ByteCount). 36 | 37 | beep(A), [X] --> 38 | { X = beep }, 39 | { Z = beep, 40 | quux(Z), _W = [ 110,110,32] }, 41 | string_without(`foo`, A). 42 | 43 | emit_reified_(To, term_begin(_, _, Func, _, Parens)) => 44 | ( is_operator(Func) 45 | -> format(To, "~w", [Func]) 46 | ; format(To, "~q", [Func]) ), 47 | ( Parens = true 48 | -> format(To, "(", []) 49 | ; true). 50 | 51 | /* 52 | weird_quasi(Quasi) :- 53 | Quasi = {|html(X)||{{X}}|}. 54 | */ 55 | 56 | burf :- 57 | A = _{ 58 | a: 1, b: [x, "y", 'Z'|Tail], 'C': x{x: 1, b: 2} 59 | }, 60 | write([1,2|_]),write([1,2|[x]]), 61 | B = foo{q: q}, 62 | C = Something{x: y}, 63 | Tail = [gurf], 64 | write(A.b), 65 | Something = aoeu, 66 | format("~q", C), 67 | display(B). 68 | 69 | eval_20(Eq,RetType,Depth,Self,[V|VI],VVO):- \+ is_list(VI),!, 70 | eval_args(Eq,RetType,Depth,Self,VI,VM), 71 | ( VM\==VI -> eval_args(Eq,RetType,Depth,Self,[V|VM],VVO) ; 72 | (eval_args(Eq,RetType,Depth,Self,V,VV), (V\==VV -> eval_args(Eq,RetType,Depth,Self,[VV|VI],VVO) ; VVO = [V|VI]))). 73 | 74 | foo(A) :- 75 | findall(X, ( 76 | member(X, A), 77 | 78 | 0 is X mod 2 79 | ), 80 | _). 81 | 82 | bar(A) :- 83 | findall(X, ( 84 | member(X, A), 85 | 0 is X mod 2 86 | ), 87 | _). 88 | 89 | bas(A) :- 90 | findall(X, ( member(X, A), 91 | 0 is X mod 2 92 | ), 93 | _). 94 | 95 | baz(A) :- 96 | findall(X, 97 | ( member(X, A), 98 | 0 is X mod 2 99 | ), 100 | _). 101 | 102 | whitespace_indentation_for_state(State, Indent) :- 103 | get_dict(state, State, Stack), 104 | aggregate_all(count, 105 | ( member(X, Stack), 106 | memberchk(X, [parens_begin, braces_begin, term_begin(_, _, _)]) ), 107 | ParensCount), 108 | Indent is ParensCount * 2 + _ToplevelIndent. 109 | 110 | %aoeu 111 | hello(A) :- 112 | ( A 113 | -> gurf 114 | ; burf 115 | ). 116 | 117 | send_message(Stream, Msg) :- 118 | put_dict(jsonrpc, Msg, "2.0", VersionedMsg), 119 | atom_json_dict(JsonCodes, VersionedMsg, [as(codes), width(0)]), 120 | phrase(utf8_codes(JsonCodes), UTF8Codes), 121 | length(UTF8Codes, ContentLength), 122 | format(Stream, "Content-Length: ~w\r\n\r\n~s", [ContentLength, JsonCodes]), 123 | flush_output(Stream). 124 | 125 | aoeuoaeuoeau(A) :- 126 | A = [0'a, 0xa, 1e-3, 0.001]. 127 | 128 | expand_subterm_positions(Term, _TermState, term_position(_From, _To, FFrom, FTo, SubPoses), 129 | Expanded, ExTail), functor(Term, ',', _, _) => 130 | % special-case comma terms to be reified as commas 131 | Expanded = [comma(FFrom, FTo)|ExpandedTail0], % aligned 132 | functor(Term, _, Arity, _), % comments 133 | expand_term_subterms_positions(false, Term, Arity, 1, SubPoses, ExpandedTail0, ExTail). 134 | 135 | foo(A, B, C, D, E) :- 136 | ( A = 1 137 | -> B = 2 138 | ; ( C = 3 139 | -> D = 4 140 | ; E = 5 ) ). 141 | 142 | hello(X) :- 143 | Aa = foo, 144 | Bb = bar, 145 | X = Aa:Bb. 146 | 147 | % end comment 148 | -------------------------------------------------------------------------------- /test/highlights.plt: -------------------------------------------------------------------------------- 1 | :- module(highlights_t, []). 2 | 3 | :- use_module(library(plunit)). 4 | 5 | :- include('../prolog/_lsp_path_add.pl'). 6 | :- use_module(lsp(lsp_highlights)). 7 | 8 | :- use_module(library(filesex), [ relative_file_name/3 ]). 9 | :- use_module(library(readutil), [ read_file_to_string/3 ]). 10 | 11 | :- begin_tests(highlights). 12 | 13 | test('Highlighting var in file location 1', 14 | [ true(Highlights =@= [_{range:_{end:_{character:55, line:33}, 15 | start:_{character:46, line:33}}}, 16 | _{range:_{end:_{character:60, line:34}, 17 | start:_{character:51, line:34}}}]) ]) :- 18 | module_property(formatter_t, file(ThisFile)), 19 | relative_file_name(InputFile, ThisFile, './highlight_input1.pl'), 20 | highlights_at_position(InputFile, line_char(34, 51), Highlights). 21 | 22 | 23 | test('Highlighting var in file location 2', 24 | [ true(Highlights =@= [_{range:_{end:_{character:5, line:56}, 25 | start:_{character:4, line:56}}}, 26 | _{range:_{end:_{character:11, line:63}, 27 | start:_{character:10, line:63}}}]) ]) :- 28 | module_property(formatter_t, file(ThisFile)), 29 | relative_file_name(InputFile, ThisFile, './highlight_input1.pl'), 30 | highlights_at_position(InputFile, line_char(57, 4), Highlights). 31 | 32 | test('Highlighting var in file location 3 - list tail', 33 | [ true(Highlights =@= [_{range:_{end:_{character:47, line:130}, 34 | start:_{character:34, line:130}}}, 35 | _{range:_{end:_{character:81, line:132}, 36 | start:_{character:68, line:132}}}]) ]) :- 37 | module_property(formatter_t, file(ThisFile)), 38 | relative_file_name(InputFile, ThisFile, './highlight_input1.pl'), 39 | highlights_at_position(InputFile, line_char(131, 45), Highlights). 40 | 41 | test('Highlighting var in file location 4 - list element', 42 | [ true(Highlights =@= [_{range:_{end:_{character:89, line:127}, 43 | start:_{character:81, line:127}}}, 44 | _{range:_{end:_{character:66, line:132}, 45 | start:_{character:58, line:132}}}]) ]) :- 46 | module_property(formatter_t, file(ThisFile)), 47 | relative_file_name(InputFile, ThisFile, './highlight_input1.pl'), 48 | highlights_at_position(InputFile, line_char(133, 58), Highlights). 49 | 50 | test('Highlighting term in file', 51 | [ true(Highlights =@= [_{range:_{end:_{character:13, line:24}, 52 | start:_{character:4, line:24}}}, 53 | _{range:_{end:_{character:29, line:105}, 54 | start:_{character:20, line:105}}}]) ]) :- 55 | module_property(formatter_t, file(ThisFile)), 56 | relative_file_name(InputFile, ThisFile, './highlight_input1.pl'), 57 | highlights_at_position(InputFile, line_char(25, 5), Highlights). 58 | 59 | %%% 60 | 61 | test('Can ask for the type of leaf to find', 62 | [ true(TheVar-Highlights =@= 63 | 'LineCount'-[_{range:_{end:_{character:55, line:33}, 64 | start:_{character:46, line:33}}}, 65 | _{range:_{end:_{character:60, line:34}, 66 | start:_{character:51, line:34}}}]) ]) :- 67 | module_property(formatter_t, file(ThisFile)), 68 | relative_file_name(InputFile, ThisFile, './highlight_input1.pl'), 69 | lsp_highlights:highlights_at_position(InputFile, line_char(34, 51), 70 | '$var'(TheVar), Highlights). 71 | 72 | test('Highlighting asking for a variable on a term should fail', 73 | [ fail ]) :- 74 | module_property(formatter_t, file(ThisFile)), 75 | relative_file_name(InputFile, ThisFile, './highlight_input1.pl'), 76 | lsp_highlights:highlights_at_position(InputFile, line_char(25, 5), '$var'(_), _). 77 | 78 | test('Highlighting asking for a variable on a comment should fail', 79 | [ fail ]) :- 80 | module_property(formatter_t, file(ThisFile)), 81 | relative_file_name(InputFile, ThisFile, './highlight_input1.pl'), 82 | lsp_highlights:highlights_at_position(InputFile, line_char(130, 14), '$var'(_), _). 83 | 84 | test('Highlighting issue with compound', 85 | [ true(Leaf-Highlights =@= '$var'('Bb')-[ 86 | _{range:_{start:_{line: 143, character: 4}, 87 | end:_{line: 143, character: 6}}}, 88 | _{range:_{start:_{line: 144, character: 11}, 89 | end:_{line: 144, character: 13}}}]) ]) :- 90 | module_property(formatter_t, file(ThisFile)), 91 | relative_file_name(InputFile, ThisFile, './highlight_input1.pl'), 92 | lsp_highlights:highlights_at_position(InputFile, line_char(145, 11), Leaf, Highlights). 93 | 94 | 95 | 96 | :- end_tests(highlights). 97 | -------------------------------------------------------------------------------- /test/server.plt: -------------------------------------------------------------------------------- 1 | :- module(server_t, []). 2 | 3 | :- use_module(library(plunit)). 4 | 5 | :- include('../prolog/_lsp_path_add.pl'). 6 | :- use_module(lsp(lsp_parser)). 7 | 8 | :- begin_tests(parsing). 9 | 10 | test('Parsing content') :- 11 | S = `Content-Length: 100\r\n\r\n{"jsonrpc": "2.0", 12 | "id": 1, 13 | "method": "textDocument/didOpen", 14 | "params": { 15 | "thing": 1 16 | } 17 | }`, phrase(lsp_request(Req), S), 18 | _{headers: _, 19 | body: _{jsonrpc: "2.0", 20 | id: 1, 21 | method: "textDocument/didOpen", 22 | params: _{thing: 1}}} :< Req. 23 | 24 | 25 | :- end_tests(parsing). 26 | -------------------------------------------------------------------------------- /test/utils.plt: -------------------------------------------------------------------------------- 1 | :- module(utils_t, []). 2 | 3 | :- use_module(library(plunit)). 4 | 5 | :- use_module(library(apply), [maplist/3]). 6 | :- use_module(library(apply_macros)). 7 | :- use_module(library(yall)). 8 | 9 | :- include('../prolog/_lsp_path_add.pl'). 10 | 11 | :- use_module(lsp(lsp_utils)). 12 | 13 | :- begin_tests(utils). 14 | 15 | ordered_locations(Ds0, Ds) :- 16 | maplist([D, SL-D]>>( get_dict(range, D, Range), 17 | get_dict(start, Range, Start), 18 | get_dict(line, Start, SL) ), Ds0, Ds1), 19 | sort(1, @=<, Ds1, Ds2), 20 | maplist([_-D, D]>>true, Ds2, Ds). 21 | 22 | test('Basic finding', 23 | [ true( Locations =@= [_{range: _{start: _{line: 8, character: 4}, 24 | end: _{line: 8, character: 29}}}, 25 | _{range: _{start: _{line: 28, character: 4}, 26 | end: _{line: 28, character: 29}}}, 27 | _{range: _{start: _{line: 29, character: 4}, 28 | end: _{line: 29, character: 29}}}, 29 | _{range: _{start: _{line: 35, character: 4}, 30 | end: _{line: 35, character: 29}}}, 31 | _{range: _{start: _{line: 36, character: 4}, 32 | end: _{line: 36, character: 29}}} 33 | ] ) ]) :- 34 | module_property(formatter_t, file(ThisFile)), 35 | relative_file_name(InputFile, ThisFile, './utils_input1.pl'), 36 | setup_call_cleanup( 37 | xref_source(InputFile), 38 | called_at(InputFile, file_offset_line_position/4, Locations0), 39 | xref_clean(InputFile) 40 | ), 41 | ordered_locations(Locations0, Locations). 42 | 43 | test('Finding called-by from meta-calls', 44 | [ true( Locations =@= [_{range: _{start: _{line: 20, character: 7}, 45 | end: _{line: 20, character: 25}}}, 46 | _{range: _{start: _{line: 25, character: 12}, 47 | end: _{line: 25, character: 30}}}] ) ]) :- 48 | module_property(formatter_t, file(ThisFile)), 49 | relative_file_name(InputFile, ThisFile, './utils_input1.pl'), 50 | setup_call_cleanup( 51 | xref_source(InputFile), 52 | called_at(InputFile, xposition_to_match/3, Locations0), 53 | xref_clean(InputFile) 54 | ), 55 | ordered_locations(Locations0, Locations). 56 | 57 | test('finding dcg', 58 | [ true( Locations =@= [_{range: _{start: _{line: 18, character: 4}, 59 | end: _{line: 18, character: 10}}}, 60 | _{range: _{start: _{line: 20, character: 4}, 61 | end: _{line: 20, character: 10}}}, 62 | _{range: _{start: _{line: 23, character: 18}, 63 | end: _{line: 23, character: 24}}}] ) ]) :- 64 | module_property(formatter_t, file(ThisFile)), 65 | relative_file_name(InputFile, ThisFile, './utils_input2.pl'), 66 | setup_call_cleanup( 67 | xref_source(InputFile), 68 | called_at(InputFile, header/1, Locations0), 69 | xref_clean(InputFile) 70 | ), 71 | ordered_locations(Locations0, Locations). 72 | 73 | :- end_tests(utils). 74 | -------------------------------------------------------------------------------- /test/utils_input1.pl: -------------------------------------------------------------------------------- 1 | :- module(utils_input1, []). 2 | 3 | :- use_module(library(apply)). 4 | 5 | xhighlights_at_position(Path, line_char(Line1, Char0), Leaf, Highlights) :- 6 | file_lines_start_end(Path, LineCharRange), 7 | read_term_positions(Path, TermsWithPositions), 8 | % find the top-level term that the offset falls within 9 | file_offset_line_position(LineCharRange, Offset, Line1, Char0), 10 | % find the specific sub-term containing the point 11 | member(TermInfo, TermsWithPositions), 12 | SubTermPoses = TermInfo.subterm, 13 | arg(1, SubTermPoses, TermFrom), 14 | arg(2, SubTermPoses, TermTo), 15 | between(TermFrom, TermTo, Offset), !, 16 | xposition_to_match(LineCharRange), % shouldn't match 17 | subterm_leaf_position(TermInfo.term, Offset, SubTermPoses, Leaf), 18 | ( Leaf = '$var'(_) 19 | % if it's a variable, only look inside the containing term 20 | -> find_occurrences_of_var(Leaf, TermInfo, Matches), 21 | xposition_to_match(LineCharRange, _, _) % another match 22 | % if it's the functor of a term, find all occurrences in the file 23 | ; functor(Leaf, FuncName, Arity), 24 | find_occurrences_of_func(FuncName, Arity, TermsWithPositions, Matches) 25 | ), 26 | maplist(xposition_to_match(LineCharRange), Matches, Highlights). 27 | 28 | xposition_to_match(LineCharRange, found_at(_, From-To), Match) :- !, 29 | file_offset_line_position(LineCharRange, From, FromLine1, FromCharacter), 30 | file_offset_line_position(LineCharRange, To, ToLine1, ToCharacter), 31 | succ(FromLine0, FromLine1), 32 | succ(ToLine0, ToLine1), 33 | Match = _{range: _{start: _{line: FromLine0, character: FromCharacter}, 34 | end: _{line: ToLine0, character: ToCharacter}}}. 35 | xposition_to_match(LineCharRange, found_at(_, term_position(_, _, FFrom, FTo, _)), Match) :- 36 | file_offset_line_position(LineCharRange, FFrom, FromLine1, FromCharacter), 37 | file_offset_line_position(LineCharRange, FTo, ToLine1, ToCharacter), 38 | succ(FromLine0, FromLine1), 39 | succ(ToLine0, ToLine1), 40 | Match = _{range: _{start: _{line: FromLine0, character: FromCharacter}, 41 | end: _{line: ToLine0, character: ToCharacter}}}. 42 | -------------------------------------------------------------------------------- /test/utils_input2.pl: -------------------------------------------------------------------------------- 1 | :- module(lsp_parser, [lsp_request//1]). 2 | /** LSP Parser 3 | 4 | Module for parsing the body & headers from an LSP client. 5 | 6 | @author James Cash 7 | */ 8 | 9 | :- use_module(library(assoc), [list_to_assoc/2, get_assoc/3]). 10 | :- use_module(library(codesio), [open_codes_stream/2]). 11 | :- use_module(library(dcg/basics), [string_without//2]). 12 | :- use_module(library(http/json), [json_read_dict/3]). 13 | 14 | header(Key-Value) --> 15 | string_without(":", KeyC), ": ", string_without("\r", ValueC), 16 | { string_codes(Key, KeyC), string_codes(Value, ValueC) }. 17 | 18 | headers([Header]) --> 19 | header(Header), "\r\n\r\n", !. 20 | headers([Header|Headers]) --> 21 | header(Header), "\r\n", 22 | headers(Headers). 23 | 24 | other_thing(H) :- header(H, _, _). 25 | 26 | lsp_request(_{headers: Headers, body: Body}) --> 27 | headers(HeadersList), 28 | { list_to_assoc(HeadersList, Headers), 29 | get_assoc("Content-Length", Headers, LengthS), 30 | number_string(Length, LengthS), 31 | length(JsonCodes, Length) }, 32 | JsonCodes, 33 | { ground(JsonCodes), 34 | open_codes_stream(JsonCodes, JsonStream), 35 | json_read_dict(JsonStream, Body, []) }. 36 | -------------------------------------------------------------------------------- /vscode/.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | -------------------------------------------------------------------------------- /vscode/.vscode/settings.json: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /vscode/README.md: -------------------------------------------------------------------------------- 1 | # Prolog LSP 2 | 3 | VSCode client for Prolog LSP server 4 | -------------------------------------------------------------------------------- /vscode/extension.js: -------------------------------------------------------------------------------- 1 | const vscode = require('vscode'); 2 | const lsp = require('vscode-languageclient'); 3 | 4 | function activate(context) { 5 | 6 | let serverOptions = { 7 | run: {command: "swipl", 8 | args: ["-g", "use_module(library(lsp_server)).", 9 | "-g", "lsp_server:main", 10 | "-t", "halt", 11 | "--", "stdio"]}, 12 | debug: {command: "swipl", 13 | args: ["-g", "use_module(library(debug)).", 14 | "-g", "debug(server(high)).", 15 | "-g", "use_module(library(lsp_server)).", 16 | "-g", "lsp_server:main", 17 | "-t", "halt", 18 | "--", "stdio"]} 19 | }; 20 | 21 | let clientOptions = { 22 | documentSelector: [{scheme: "file", language: "prolog"}], 23 | }; 24 | 25 | let client = new lsp.LanguageClient( 26 | 'prolog-lsp', 27 | 'Prolog Language Client', 28 | serverOptions, 29 | clientOptions 30 | ); 31 | 32 | context.subscriptions.push(client.start()); 33 | } 34 | exports.activate = activate; 35 | 36 | function deactivate(context) { 37 | if (!context.client) { return; } 38 | context.client.stop(); 39 | } 40 | exports.deactivate = deactivate; 41 | -------------------------------------------------------------------------------- /vscode/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "prolog-lsp", 3 | "version": "2.2.6", 4 | "lockfileVersion": 1, 5 | "requires": true, 6 | "dependencies": { 7 | "@tootallnate/once": { 8 | "version": "1.1.2", 9 | "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-1.1.2.tgz", 10 | "integrity": "sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw==", 11 | "dev": true 12 | }, 13 | "agent-base": { 14 | "version": "6.0.2", 15 | "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", 16 | "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", 17 | "dev": true, 18 | "requires": { 19 | "debug": "4" 20 | } 21 | }, 22 | "balanced-match": { 23 | "version": "1.0.0", 24 | "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", 25 | "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", 26 | "dev": true 27 | }, 28 | "brace-expansion": { 29 | "version": "1.1.11", 30 | "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", 31 | "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", 32 | "dev": true, 33 | "requires": { 34 | "balanced-match": "^1.0.0", 35 | "concat-map": "0.0.1" 36 | } 37 | }, 38 | "browser-stdout": { 39 | "version": "1.3.1", 40 | "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz", 41 | "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==", 42 | "dev": true 43 | }, 44 | "buffer-from": { 45 | "version": "1.1.1", 46 | "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz", 47 | "integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==", 48 | "dev": true 49 | }, 50 | "commander": { 51 | "version": "2.15.1", 52 | "resolved": "https://registry.npmjs.org/commander/-/commander-2.15.1.tgz", 53 | "integrity": "sha512-VlfT9F3V0v+jr4yxPc5gg9s62/fIVWsd2Bk2iD435um1NlGMYdVCq+MjcXnhYq2icNOizHr1kK+5TI6H0Hy0ag==", 54 | "dev": true 55 | }, 56 | "concat-map": { 57 | "version": "0.0.1", 58 | "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", 59 | "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", 60 | "dev": true 61 | }, 62 | "debug": { 63 | "version": "4.3.1", 64 | "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", 65 | "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", 66 | "dev": true, 67 | "requires": { 68 | "ms": "2.1.2" 69 | } 70 | }, 71 | "diff": { 72 | "version": "3.5.0", 73 | "resolved": "https://registry.npmjs.org/diff/-/diff-3.5.0.tgz", 74 | "integrity": "sha512-A46qtFgd+g7pDZinpnwiRJtxbC1hpgf0uzP3iG89scHk0AUC7A1TGxf5OiiOUv/JMZR8GOt8hL900hV0bOy5xA==", 75 | "dev": true 76 | }, 77 | "es6-promise": { 78 | "version": "4.2.8", 79 | "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-4.2.8.tgz", 80 | "integrity": "sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w==", 81 | "dev": true 82 | }, 83 | "es6-promisify": { 84 | "version": "5.0.0", 85 | "resolved": "https://registry.npmjs.org/es6-promisify/-/es6-promisify-5.0.0.tgz", 86 | "integrity": "sha1-UQnWLz5W6pZ8S2NQWu8IKRyKUgM=", 87 | "dev": true, 88 | "requires": { 89 | "es6-promise": "^4.0.3" 90 | } 91 | }, 92 | "escape-string-regexp": { 93 | "version": "1.0.5", 94 | "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", 95 | "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", 96 | "dev": true 97 | }, 98 | "fs.realpath": { 99 | "version": "1.0.0", 100 | "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", 101 | "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", 102 | "dev": true 103 | }, 104 | "glob": { 105 | "version": "7.1.6", 106 | "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", 107 | "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==", 108 | "dev": true, 109 | "requires": { 110 | "fs.realpath": "^1.0.0", 111 | "inflight": "^1.0.4", 112 | "inherits": "2", 113 | "minimatch": "^3.0.4", 114 | "once": "^1.3.0", 115 | "path-is-absolute": "^1.0.0" 116 | } 117 | }, 118 | "growl": { 119 | "version": "1.10.5", 120 | "resolved": "https://registry.npmjs.org/growl/-/growl-1.10.5.tgz", 121 | "integrity": "sha512-qBr4OuELkhPenW6goKVXiv47US3clb3/IbuWF9KNKEijAy9oeHxU9IgzjvJhHkUzhaj7rOUD7+YGWqUjLp5oSA==", 122 | "dev": true 123 | }, 124 | "has-flag": { 125 | "version": "3.0.0", 126 | "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", 127 | "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", 128 | "dev": true 129 | }, 130 | "he": { 131 | "version": "1.1.1", 132 | "resolved": "https://registry.npmjs.org/he/-/he-1.1.1.tgz", 133 | "integrity": "sha1-k0EP0hsAlzUVH4howvJx80J+I/0=", 134 | "dev": true 135 | }, 136 | "http-proxy-agent": { 137 | "version": "4.0.1", 138 | "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-4.0.1.tgz", 139 | "integrity": "sha512-k0zdNgqWTGA6aeIRVpvfVob4fL52dTfaehylg0Y4UvSySvOq/Y+BOyPrgpUrA7HylqvU8vIZGsRuXmspskV0Tg==", 140 | "dev": true, 141 | "requires": { 142 | "@tootallnate/once": "1", 143 | "agent-base": "6", 144 | "debug": "4" 145 | } 146 | }, 147 | "https-proxy-agent": { 148 | "version": "5.0.0", 149 | "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.0.tgz", 150 | "integrity": "sha512-EkYm5BcKUGiduxzSt3Eppko+PiNWNEpa4ySk9vTC6wDsQJW9rHSa+UhGNJoRYp7bz6Ht1eaRIa6QaJqO5rCFbA==", 151 | "dev": true, 152 | "requires": { 153 | "agent-base": "6", 154 | "debug": "4" 155 | } 156 | }, 157 | "inflight": { 158 | "version": "1.0.6", 159 | "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", 160 | "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", 161 | "dev": true, 162 | "requires": { 163 | "once": "^1.3.0", 164 | "wrappy": "1" 165 | } 166 | }, 167 | "inherits": { 168 | "version": "2.0.4", 169 | "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", 170 | "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", 171 | "dev": true 172 | }, 173 | "minimatch": { 174 | "version": "3.0.4", 175 | "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", 176 | "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", 177 | "dev": true, 178 | "requires": { 179 | "brace-expansion": "^1.1.7" 180 | } 181 | }, 182 | "minimist": { 183 | "version": "0.0.8", 184 | "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", 185 | "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=", 186 | "dev": true 187 | }, 188 | "mkdirp": { 189 | "version": "0.5.1", 190 | "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", 191 | "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", 192 | "dev": true, 193 | "requires": { 194 | "minimist": "0.0.8" 195 | } 196 | }, 197 | "mocha": { 198 | "version": "5.2.0", 199 | "resolved": "https://registry.npmjs.org/mocha/-/mocha-5.2.0.tgz", 200 | "integrity": "sha512-2IUgKDhc3J7Uug+FxMXuqIyYzH7gJjXECKe/w43IGgQHTSj3InJi+yAA7T24L9bQMRKiUEHxEX37G5JpVUGLcQ==", 201 | "dev": true, 202 | "requires": { 203 | "browser-stdout": "1.3.1", 204 | "commander": "2.15.1", 205 | "debug": "3.1.0", 206 | "diff": "3.5.0", 207 | "escape-string-regexp": "1.0.5", 208 | "glob": "7.1.2", 209 | "growl": "1.10.5", 210 | "he": "1.1.1", 211 | "minimatch": "3.0.4", 212 | "mkdirp": "0.5.1", 213 | "supports-color": "5.4.0" 214 | }, 215 | "dependencies": { 216 | "debug": { 217 | "version": "3.1.0", 218 | "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", 219 | "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", 220 | "dev": true, 221 | "requires": { 222 | "ms": "2.0.0" 223 | } 224 | }, 225 | "glob": { 226 | "version": "7.1.2", 227 | "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz", 228 | "integrity": "sha512-MJTUg1kjuLeQCJ+ccE4Vpa6kKVXkPYJ2mOCQyUuKLcLQsdrMCpBPUi8qVE6+YuaJkozeA9NusTAw3hLr8Xe5EQ==", 229 | "dev": true, 230 | "requires": { 231 | "fs.realpath": "^1.0.0", 232 | "inflight": "^1.0.4", 233 | "inherits": "2", 234 | "minimatch": "^3.0.4", 235 | "once": "^1.3.0", 236 | "path-is-absolute": "^1.0.0" 237 | } 238 | }, 239 | "ms": { 240 | "version": "2.0.0", 241 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", 242 | "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", 243 | "dev": true 244 | } 245 | } 246 | }, 247 | "ms": { 248 | "version": "2.1.2", 249 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", 250 | "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", 251 | "dev": true 252 | }, 253 | "once": { 254 | "version": "1.4.0", 255 | "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", 256 | "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", 257 | "dev": true, 258 | "requires": { 259 | "wrappy": "1" 260 | } 261 | }, 262 | "path-is-absolute": { 263 | "version": "1.0.1", 264 | "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", 265 | "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", 266 | "dev": true 267 | }, 268 | "semver": { 269 | "version": "5.7.1", 270 | "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", 271 | "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", 272 | "dev": true 273 | }, 274 | "source-map": { 275 | "version": "0.6.1", 276 | "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", 277 | "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", 278 | "dev": true 279 | }, 280 | "source-map-support": { 281 | "version": "0.5.19", 282 | "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.19.tgz", 283 | "integrity": "sha512-Wonm7zOCIJzBGQdB+thsPar0kYuCIzYvxZwlBa87yi/Mdjv7Tip2cyVbLj5o0cFPN4EVkuTwb3GDDyUx2DGnGw==", 284 | "dev": true, 285 | "requires": { 286 | "buffer-from": "^1.0.0", 287 | "source-map": "^0.6.0" 288 | } 289 | }, 290 | "supports-color": { 291 | "version": "5.4.0", 292 | "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.4.0.tgz", 293 | "integrity": "sha512-zjaXglF5nnWpsq470jSv6P9DwPvgLkuapYmfDm3JWOm0vkNTVF2tI4UrN2r6jH1qM/uc/WtxYY1hYoA2dOKj5w==", 294 | "dev": true, 295 | "requires": { 296 | "has-flag": "^3.0.0" 297 | } 298 | }, 299 | "vscode": { 300 | "version": "1.1.37", 301 | "resolved": "https://registry.npmjs.org/vscode/-/vscode-1.1.37.tgz", 302 | "integrity": "sha512-vJNj6IlN7IJPdMavlQa1KoFB3Ihn06q1AiN3ZFI/HfzPNzbKZWPPuiU+XkpNOfGU5k15m4r80nxNPlM7wcc0wg==", 303 | "dev": true, 304 | "requires": { 305 | "glob": "^7.1.2", 306 | "http-proxy-agent": "^4.0.1", 307 | "https-proxy-agent": "^5.0.0", 308 | "mocha": "^5.2.0", 309 | "semver": "^5.4.1", 310 | "source-map-support": "^0.5.0", 311 | "vscode-test": "^0.4.1" 312 | } 313 | }, 314 | "vscode-jsonrpc": { 315 | "version": "6.0.0", 316 | "resolved": "https://registry.npmjs.org/vscode-jsonrpc/-/vscode-jsonrpc-6.0.0.tgz", 317 | "integrity": "sha512-wnJA4BnEjOSyFMvjZdpiOwhSq9uDoK8e/kpRJDTaMYzwlkrhG1fwDIZI94CLsLzlCK5cIbMMtFlJlfR57Lavmg==" 318 | }, 319 | "vscode-languageclient": { 320 | "version": "4.4.2", 321 | "resolved": "https://registry.npmjs.org/vscode-languageclient/-/vscode-languageclient-4.4.2.tgz", 322 | "integrity": "sha512-9TUzsg1UM6n1UEyPlWbDf7tK1wJAK7UGFRmGDN8sz4KmbbDiVRh6YicaB/5oRSVTpuV47PdJpYlOl3SJ0RiK1Q==", 323 | "requires": { 324 | "vscode-languageserver-protocol": "^3.10.3" 325 | } 326 | }, 327 | "vscode-languageserver-protocol": { 328 | "version": "3.16.0", 329 | "resolved": "https://registry.npmjs.org/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.16.0.tgz", 330 | "integrity": "sha512-sdeUoAawceQdgIfTI+sdcwkiK2KU+2cbEYA0agzM2uqaUy2UpnnGHtWTHVEtS0ES4zHU0eMFRGN+oQgDxlD66A==", 331 | "requires": { 332 | "vscode-jsonrpc": "6.0.0", 333 | "vscode-languageserver-types": "3.16.0" 334 | } 335 | }, 336 | "vscode-languageserver-types": { 337 | "version": "3.16.0", 338 | "resolved": "https://registry.npmjs.org/vscode-languageserver-types/-/vscode-languageserver-types-3.16.0.tgz", 339 | "integrity": "sha512-k8luDIWJWyenLc5ToFQQMaSrqCHiLwyKPHKPQZ5zz21vM+vIVUSvsRpcbiECH4WR88K2XZqc4ScRcZ7nk/jbeA==" 340 | }, 341 | "vscode-test": { 342 | "version": "0.4.3", 343 | "resolved": "https://registry.npmjs.org/vscode-test/-/vscode-test-0.4.3.tgz", 344 | "integrity": "sha512-EkMGqBSefZH2MgW65nY05rdRSko15uvzq4VAPM5jVmwYuFQKE7eikKXNJDRxL+OITXHB6pI+a3XqqD32Y3KC5w==", 345 | "dev": true, 346 | "requires": { 347 | "http-proxy-agent": "^2.1.0", 348 | "https-proxy-agent": "^2.2.1" 349 | }, 350 | "dependencies": { 351 | "agent-base": { 352 | "version": "4.3.0", 353 | "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-4.3.0.tgz", 354 | "integrity": "sha512-salcGninV0nPrwpGNn4VTXBb1SOuXQBiqbrNXoeizJsHrsL6ERFM2Ne3JUSBWRE6aeNJI2ROP/WEEIDUiDe3cg==", 355 | "dev": true, 356 | "requires": { 357 | "es6-promisify": "^5.0.0" 358 | } 359 | }, 360 | "debug": { 361 | "version": "3.1.0", 362 | "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", 363 | "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", 364 | "dev": true, 365 | "requires": { 366 | "ms": "2.0.0" 367 | } 368 | }, 369 | "http-proxy-agent": { 370 | "version": "2.1.0", 371 | "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-2.1.0.tgz", 372 | "integrity": "sha512-qwHbBLV7WviBl0rQsOzH6o5lwyOIvwp/BdFnvVxXORldu5TmjFfjzBcWUWS5kWAZhmv+JtiDhSuQCp4sBfbIgg==", 373 | "dev": true, 374 | "requires": { 375 | "agent-base": "4", 376 | "debug": "3.1.0" 377 | } 378 | }, 379 | "https-proxy-agent": { 380 | "version": "2.2.4", 381 | "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-2.2.4.tgz", 382 | "integrity": "sha512-OmvfoQ53WLjtA9HeYP9RNrWMJzzAz1JGaSFr1nijg0PVR1JaD/xbJq1mdEIIlxGpXp9eSe/O2LgU9DJmTPd0Eg==", 383 | "dev": true, 384 | "requires": { 385 | "agent-base": "^4.3.0", 386 | "debug": "^3.1.0" 387 | } 388 | }, 389 | "ms": { 390 | "version": "2.0.0", 391 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", 392 | "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", 393 | "dev": true 394 | } 395 | } 396 | }, 397 | "wrappy": { 398 | "version": "1.0.2", 399 | "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", 400 | "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", 401 | "dev": true 402 | } 403 | } 404 | } 405 | -------------------------------------------------------------------------------- /vscode/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "prolog-lsp", 3 | "displayName": "prolog-lsp", 4 | "description": "A language server for (SWI-)Prolog", 5 | "author": "James Cash", 6 | "license": "BSD-2-Clause", 7 | "publisher": "jamesnvc", 8 | "version": "2.2.7", 9 | "categories": ["Other"], 10 | "keywords": [ 11 | "prolog" 12 | ], 13 | "engines": { 14 | "vscode": "^1.54.0" 15 | }, 16 | "repository": { 17 | "type": "git", 18 | "url": "https://github.com/jamesnvc/lsp_server" 19 | }, 20 | "activationEvents": [ 21 | "onLanguage:prolog" 22 | ], 23 | "main": "./extension", 24 | "contributes": { 25 | "languages": [ 26 | {"id": "prolog", 27 | "aliases": ["Prolog", 28 | "SWI-Prolog", 29 | "swipl"], 30 | "configuration": "./prolog.config.json", 31 | "extensions": [".pl", ".plt", ".prolog"] 32 | } 33 | ] 34 | }, 35 | "dependencies": { 36 | "vscode-languageclient": "^4.1.3" 37 | }, 38 | "devDependencies": { 39 | "vscode": "^1.1.6" 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /vscode/prolog.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "comments": { 3 | "lineComment": "%", 4 | "blockComment": ["/*", "*/"] 5 | }, 6 | "brackets": [ 7 | ["{", "}"], 8 | ["[", "]"], 9 | ["(", ")"] 10 | ], 11 | "autoClosingPairs": [ 12 | ["{", "}"], 13 | ["[", "]"], 14 | ["(", ")"], 15 | ["\"", "\""], 16 | ["'", "'"] 17 | ], 18 | "surroundingPairs": [ 19 | ["{", "}"], 20 | ["[", "]"], 21 | ["(", ")"], 22 | ["\"", "\""], 23 | ["'", "'"] 24 | ] 25 | } 26 | --------------------------------------------------------------------------------