├── .github
├── FUNDING.yml
├── dependabot.yml
└── workflows
│ └── build.yml
├── .gitignore
├── .gitlab-ci.yml
├── .gitmodules
├── COPYING
├── CastleEngineManifest.xml
├── README.md
├── client
└── nvim
│ ├── README.md
│ └── pascal.lua
└── server
├── castlearchitectures.pas
├── castlelsp.pas
├── pasls.lpi
├── pasls.lpr
├── ubufferedreader.pas
├── udebug.pas
├── udocumentsymbolsupport.pas
├── uinitialize.pas
├── ujsonrpc.pas
├── ulogvscode.pas
├── upackages.pas
├── ushutdown.pas
├── utextdocument.pas
├── uutils.pas
└── uworkspacesymbolsupport.pas
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | # These are supported funding model platforms
2 |
3 | github: [castle-engine, michaliskambi] # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
4 | patreon: castleengine # Replace with a single Patreon username
5 | open_collective: castle-engine # Replace with a single Open Collective username
6 | # ko_fi: # Replace with a single Ko-fi username
7 | # tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
8 | # community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
9 | # liberapay: # Replace with a single Liberapay username
10 | # issuehunt: # Replace with a single IssueHunt username
11 | # otechie: # Replace with a single Otechie username
12 | # custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
13 |
--------------------------------------------------------------------------------
/.github/dependabot.yml:
--------------------------------------------------------------------------------
1 | # Check that GitHub Actions use latest versions of plugins.
2 | # See https://docs.github.com/en/code-security/dependabot/working-with-dependabot/keeping-your-actions-up-to-date-with-dependabot .
3 |
4 | version: 2
5 | updates:
6 | # Maintain dependencies for GitHub Actions
7 | - package-ecosystem: "github-actions"
8 | directory: "/"
9 | schedule:
10 | interval: "weekly"
11 |
--------------------------------------------------------------------------------
/.github/workflows/build.yml:
--------------------------------------------------------------------------------
1 | # ----------------------------------------------------------------------------
2 | # GitHub Actions workflow to test building pasls (server).
3 | #
4 | # To check
5 | # - whether it builds,
6 | # - whether the FPC and Lazarus versions in our
7 | # Docker image are sufficient.
8 | # (Right now, IdentComplIncludeKeywords requires Lazarus >= 3.0.0).
9 | #
10 | # This uses GitHub-hosted runners, that is: you don't need to set up any server
11 | # infrastructure, GitHub provides it all for free for open-source projects.
12 | #
13 | # See docs:
14 | # - https://castle-engine.io/github_actions
15 | # - https://docs.github.com/en/actions
16 | # ----------------------------------------------------------------------------
17 |
18 | name: Build
19 | on: [push, pull_request]
20 |
21 | jobs:
22 | build-in-docker:
23 | name: Build (Docker)
24 | runs-on: ubuntu-latest
25 | container: kambi/castle-engine-cloud-builds-tools:cge-none
26 | steps:
27 | - uses: actions/checkout@v4
28 | with:
29 | submodules: 'recursive'
30 | - name: Compile Server
31 | run: lazbuild server/pasls.lpi
32 |
33 | build:
34 | name: Build (Not Docker)
35 | strategy:
36 | matrix:
37 | operating-system: [windows-latest, ubuntu-latest, macos-latest]
38 | lazarus-versions: [stable, "3.0"]
39 | runs-on: ${{ matrix.operating-system }}
40 | steps:
41 | - uses: actions/checkout@v4
42 | with:
43 | submodules: 'recursive'
44 | - name: Install FPC+Lazarus
45 | uses: gcarreno/setup-lazarus@v3.2.17
46 | with:
47 | lazarus-version: ${{ matrix.lazarus-versions }}
48 | # Having issues with cache in https://github.com/castle-engine/pascal-language-server/actions/runs/8438772805
49 | # on Linux and macOS (it is disabled on Windows by default already)
50 | with-cache: false
51 | - name: Compile Server
52 | run: lazbuild server/pasls.lpi
53 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | /backup/
2 | /lib/
3 | /server/lib/
4 | *.lps
5 | *.dbg
6 | *~
7 | /server/pasls
8 | /server/pasls.exe
9 |
10 | server/backup/
11 |
--------------------------------------------------------------------------------
/.gitlab-ci.yml:
--------------------------------------------------------------------------------
1 | stages:
2 | - build
3 |
4 | fedora-build:
5 | stage: build
6 | image: fedora-lazarus
7 | script:
8 | - cd server
9 | - lazbuild pasls.lpr
10 | variables:
11 | GIT_SUBMODULE_STRATEGY: recursive
12 |
--------------------------------------------------------------------------------
/.gitmodules:
--------------------------------------------------------------------------------
1 | [submodule "deps/jsonstream"]
2 | path = server/deps/jsonstream
3 | url = https://github.com/Isopod/jsonstream.git
4 |
--------------------------------------------------------------------------------
/COPYING:
--------------------------------------------------------------------------------
1 | GNU GENERAL PUBLIC LICENSE
2 | Version 3, 29 June 2007
3 |
4 | Copyright (C) 2007 Free Software Foundation, Inc.
5 | Everyone is permitted to copy and distribute verbatim copies
6 | of this license document, but changing it is not allowed.
7 |
8 | Preamble
9 |
10 | The GNU General Public License is a free, copyleft license for
11 | software and other kinds of works.
12 |
13 | The licenses for most software and other practical works are designed
14 | to take away your freedom to share and change the works. By contrast,
15 | the GNU General Public License is intended to guarantee your freedom to
16 | share and change all versions of a program--to make sure it remains free
17 | software for all its users. We, the Free Software Foundation, use the
18 | GNU General Public License for most of our software; it applies also to
19 | any other work released this way by its authors. You can apply it to
20 | your programs, too.
21 |
22 | When we speak of free software, we are referring to freedom, not
23 | price. Our General Public Licenses are designed to make sure that you
24 | have the freedom to distribute copies of free software (and charge for
25 | them if you wish), that you receive source code or can get it if you
26 | want it, that you can change the software or use pieces of it in new
27 | free programs, and that you know you can do these things.
28 |
29 | To protect your rights, we need to prevent others from denying you
30 | these rights or asking you to surrender the rights. Therefore, you have
31 | certain responsibilities if you distribute copies of the software, or if
32 | you modify it: responsibilities to respect the freedom of others.
33 |
34 | For example, if you distribute copies of such a program, whether
35 | gratis or for a fee, you must pass on to the recipients the same
36 | freedoms that you received. You must make sure that they, too, receive
37 | or can get the source code. And you must show them these terms so they
38 | know their rights.
39 |
40 | Developers that use the GNU GPL protect your rights with two steps:
41 | (1) assert copyright on the software, and (2) offer you this License
42 | giving you legal permission to copy, distribute and/or modify it.
43 |
44 | For the developers' and authors' protection, the GPL clearly explains
45 | that there is no warranty for this free software. For both users' and
46 | authors' sake, the GPL requires that modified versions be marked as
47 | changed, so that their problems will not be attributed erroneously to
48 | authors of previous versions.
49 |
50 | Some devices are designed to deny users access to install or run
51 | modified versions of the software inside them, although the manufacturer
52 | can do so. This is fundamentally incompatible with the aim of
53 | protecting users' freedom to change the software. The systematic
54 | pattern of such abuse occurs in the area of products for individuals to
55 | use, which is precisely where it is most unacceptable. Therefore, we
56 | have designed this version of the GPL to prohibit the practice for those
57 | products. If such problems arise substantially in other domains, we
58 | stand ready to extend this provision to those domains in future versions
59 | of the GPL, as needed to protect the freedom of users.
60 |
61 | Finally, every program is threatened constantly by software patents.
62 | States should not allow patents to restrict development and use of
63 | software on general-purpose computers, but in those that do, we wish to
64 | avoid the special danger that patents applied to a free program could
65 | make it effectively proprietary. To prevent this, the GPL assures that
66 | patents cannot be used to render the program non-free.
67 |
68 | The precise terms and conditions for copying, distribution and
69 | modification follow.
70 |
71 | TERMS AND CONDITIONS
72 |
73 | 0. Definitions.
74 |
75 | "This License" refers to version 3 of the GNU General Public License.
76 |
77 | "Copyright" also means copyright-like laws that apply to other kinds of
78 | works, such as semiconductor masks.
79 |
80 | "The Program" refers to any copyrightable work licensed under this
81 | License. Each licensee is addressed as "you". "Licensees" and
82 | "recipients" may be individuals or organizations.
83 |
84 | To "modify" a work means to copy from or adapt all or part of the work
85 | in a fashion requiring copyright permission, other than the making of an
86 | exact copy. The resulting work is called a "modified version" of the
87 | earlier work or a work "based on" the earlier work.
88 |
89 | A "covered work" means either the unmodified Program or a work based
90 | on the Program.
91 |
92 | To "propagate" a work means to do anything with it that, without
93 | permission, would make you directly or secondarily liable for
94 | infringement under applicable copyright law, except executing it on a
95 | computer or modifying a private copy. Propagation includes copying,
96 | distribution (with or without modification), making available to the
97 | public, and in some countries other activities as well.
98 |
99 | To "convey" a work means any kind of propagation that enables other
100 | parties to make or receive copies. Mere interaction with a user through
101 | a computer network, with no transfer of a copy, is not conveying.
102 |
103 | An interactive user interface displays "Appropriate Legal Notices"
104 | to the extent that it includes a convenient and prominently visible
105 | feature that (1) displays an appropriate copyright notice, and (2)
106 | tells the user that there is no warranty for the work (except to the
107 | extent that warranties are provided), that licensees may convey the
108 | work under this License, and how to view a copy of this License. If
109 | the interface presents a list of user commands or options, such as a
110 | menu, a prominent item in the list meets this criterion.
111 |
112 | 1. Source Code.
113 |
114 | The "source code" for a work means the preferred form of the work
115 | for making modifications to it. "Object code" means any non-source
116 | form of a work.
117 |
118 | A "Standard Interface" means an interface that either is an official
119 | standard defined by a recognized standards body, or, in the case of
120 | interfaces specified for a particular programming language, one that
121 | is widely used among developers working in that language.
122 |
123 | The "System Libraries" of an executable work include anything, other
124 | than the work as a whole, that (a) is included in the normal form of
125 | packaging a Major Component, but which is not part of that Major
126 | Component, and (b) serves only to enable use of the work with that
127 | Major Component, or to implement a Standard Interface for which an
128 | implementation is available to the public in source code form. A
129 | "Major Component", in this context, means a major essential component
130 | (kernel, window system, and so on) of the specific operating system
131 | (if any) on which the executable work runs, or a compiler used to
132 | produce the work, or an object code interpreter used to run it.
133 |
134 | The "Corresponding Source" for a work in object code form means all
135 | the source code needed to generate, install, and (for an executable
136 | work) run the object code and to modify the work, including scripts to
137 | control those activities. However, it does not include the work's
138 | System Libraries, or general-purpose tools or generally available free
139 | programs which are used unmodified in performing those activities but
140 | which are not part of the work. For example, Corresponding Source
141 | includes interface definition files associated with source files for
142 | the work, and the source code for shared libraries and dynamically
143 | linked subprograms that the work is specifically designed to require,
144 | such as by intimate data communication or control flow between those
145 | subprograms and other parts of the work.
146 |
147 | The Corresponding Source need not include anything that users
148 | can regenerate automatically from other parts of the Corresponding
149 | Source.
150 |
151 | The Corresponding Source for a work in source code form is that
152 | same work.
153 |
154 | 2. Basic Permissions.
155 |
156 | All rights granted under this License are granted for the term of
157 | copyright on the Program, and are irrevocable provided the stated
158 | conditions are met. This License explicitly affirms your unlimited
159 | permission to run the unmodified Program. The output from running a
160 | covered work is covered by this License only if the output, given its
161 | content, constitutes a covered work. This License acknowledges your
162 | rights of fair use or other equivalent, as provided by copyright law.
163 |
164 | You may make, run and propagate covered works that you do not
165 | convey, without conditions so long as your license otherwise remains
166 | in force. You may convey covered works to others for the sole purpose
167 | of having them make modifications exclusively for you, or provide you
168 | with facilities for running those works, provided that you comply with
169 | the terms of this License in conveying all material for which you do
170 | not control copyright. Those thus making or running the covered works
171 | for you must do so exclusively on your behalf, under your direction
172 | and control, on terms that prohibit them from making any copies of
173 | your copyrighted material outside their relationship with you.
174 |
175 | Conveying under any other circumstances is permitted solely under
176 | the conditions stated below. Sublicensing is not allowed; section 10
177 | makes it unnecessary.
178 |
179 | 3. Protecting Users' Legal Rights From Anti-Circumvention Law.
180 |
181 | No covered work shall be deemed part of an effective technological
182 | measure under any applicable law fulfilling obligations under article
183 | 11 of the WIPO copyright treaty adopted on 20 December 1996, or
184 | similar laws prohibiting or restricting circumvention of such
185 | measures.
186 |
187 | When you convey a covered work, you waive any legal power to forbid
188 | circumvention of technological measures to the extent such circumvention
189 | is effected by exercising rights under this License with respect to
190 | the covered work, and you disclaim any intention to limit operation or
191 | modification of the work as a means of enforcing, against the work's
192 | users, your or third parties' legal rights to forbid circumvention of
193 | technological measures.
194 |
195 | 4. Conveying Verbatim Copies.
196 |
197 | You may convey verbatim copies of the Program's source code as you
198 | receive it, in any medium, provided that you conspicuously and
199 | appropriately publish on each copy an appropriate copyright notice;
200 | keep intact all notices stating that this License and any
201 | non-permissive terms added in accord with section 7 apply to the code;
202 | keep intact all notices of the absence of any warranty; and give all
203 | recipients a copy of this License along with the Program.
204 |
205 | You may charge any price or no price for each copy that you convey,
206 | and you may offer support or warranty protection for a fee.
207 |
208 | 5. Conveying Modified Source Versions.
209 |
210 | You may convey a work based on the Program, or the modifications to
211 | produce it from the Program, in the form of source code under the
212 | terms of section 4, provided that you also meet all of these conditions:
213 |
214 | a) The work must carry prominent notices stating that you modified
215 | it, and giving a relevant date.
216 |
217 | b) The work must carry prominent notices stating that it is
218 | released under this License and any conditions added under section
219 | 7. This requirement modifies the requirement in section 4 to
220 | "keep intact all notices".
221 |
222 | c) You must license the entire work, as a whole, under this
223 | License to anyone who comes into possession of a copy. This
224 | License will therefore apply, along with any applicable section 7
225 | additional terms, to the whole of the work, and all its parts,
226 | regardless of how they are packaged. This License gives no
227 | permission to license the work in any other way, but it does not
228 | invalidate such permission if you have separately received it.
229 |
230 | d) If the work has interactive user interfaces, each must display
231 | Appropriate Legal Notices; however, if the Program has interactive
232 | interfaces that do not display Appropriate Legal Notices, your
233 | work need not make them do so.
234 |
235 | A compilation of a covered work with other separate and independent
236 | works, which are not by their nature extensions of the covered work,
237 | and which are not combined with it such as to form a larger program,
238 | in or on a volume of a storage or distribution medium, is called an
239 | "aggregate" if the compilation and its resulting copyright are not
240 | used to limit the access or legal rights of the compilation's users
241 | beyond what the individual works permit. Inclusion of a covered work
242 | in an aggregate does not cause this License to apply to the other
243 | parts of the aggregate.
244 |
245 | 6. Conveying Non-Source Forms.
246 |
247 | You may convey a covered work in object code form under the terms
248 | of sections 4 and 5, provided that you also convey the
249 | machine-readable Corresponding Source under the terms of this License,
250 | in one of these ways:
251 |
252 | a) Convey the object code in, or embodied in, a physical product
253 | (including a physical distribution medium), accompanied by the
254 | Corresponding Source fixed on a durable physical medium
255 | customarily used for software interchange.
256 |
257 | b) Convey the object code in, or embodied in, a physical product
258 | (including a physical distribution medium), accompanied by a
259 | written offer, valid for at least three years and valid for as
260 | long as you offer spare parts or customer support for that product
261 | model, to give anyone who possesses the object code either (1) a
262 | copy of the Corresponding Source for all the software in the
263 | product that is covered by this License, on a durable physical
264 | medium customarily used for software interchange, for a price no
265 | more than your reasonable cost of physically performing this
266 | conveying of source, or (2) access to copy the
267 | Corresponding Source from a network server at no charge.
268 |
269 | c) Convey individual copies of the object code with a copy of the
270 | written offer to provide the Corresponding Source. This
271 | alternative is allowed only occasionally and noncommercially, and
272 | only if you received the object code with such an offer, in accord
273 | with subsection 6b.
274 |
275 | d) Convey the object code by offering access from a designated
276 | place (gratis or for a charge), and offer equivalent access to the
277 | Corresponding Source in the same way through the same place at no
278 | further charge. You need not require recipients to copy the
279 | Corresponding Source along with the object code. If the place to
280 | copy the object code is a network server, the Corresponding Source
281 | may be on a different server (operated by you or a third party)
282 | that supports equivalent copying facilities, provided you maintain
283 | clear directions next to the object code saying where to find the
284 | Corresponding Source. Regardless of what server hosts the
285 | Corresponding Source, you remain obligated to ensure that it is
286 | available for as long as needed to satisfy these requirements.
287 |
288 | e) Convey the object code using peer-to-peer transmission, provided
289 | you inform other peers where the object code and Corresponding
290 | Source of the work are being offered to the general public at no
291 | charge under subsection 6d.
292 |
293 | A separable portion of the object code, whose source code is excluded
294 | from the Corresponding Source as a System Library, need not be
295 | included in conveying the object code work.
296 |
297 | A "User Product" is either (1) a "consumer product", which means any
298 | tangible personal property which is normally used for personal, family,
299 | or household purposes, or (2) anything designed or sold for incorporation
300 | into a dwelling. In determining whether a product is a consumer product,
301 | doubtful cases shall be resolved in favor of coverage. For a particular
302 | product received by a particular user, "normally used" refers to a
303 | typical or common use of that class of product, regardless of the status
304 | of the particular user or of the way in which the particular user
305 | actually uses, or expects or is expected to use, the product. A product
306 | is a consumer product regardless of whether the product has substantial
307 | commercial, industrial or non-consumer uses, unless such uses represent
308 | the only significant mode of use of the product.
309 |
310 | "Installation Information" for a User Product means any methods,
311 | procedures, authorization keys, or other information required to install
312 | and execute modified versions of a covered work in that User Product from
313 | a modified version of its Corresponding Source. The information must
314 | suffice to ensure that the continued functioning of the modified object
315 | code is in no case prevented or interfered with solely because
316 | modification has been made.
317 |
318 | If you convey an object code work under this section in, or with, or
319 | specifically for use in, a User Product, and the conveying occurs as
320 | part of a transaction in which the right of possession and use of the
321 | User Product is transferred to the recipient in perpetuity or for a
322 | fixed term (regardless of how the transaction is characterized), the
323 | Corresponding Source conveyed under this section must be accompanied
324 | by the Installation Information. But this requirement does not apply
325 | if neither you nor any third party retains the ability to install
326 | modified object code on the User Product (for example, the work has
327 | been installed in ROM).
328 |
329 | The requirement to provide Installation Information does not include a
330 | requirement to continue to provide support service, warranty, or updates
331 | for a work that has been modified or installed by the recipient, or for
332 | the User Product in which it has been modified or installed. Access to a
333 | network may be denied when the modification itself materially and
334 | adversely affects the operation of the network or violates the rules and
335 | protocols for communication across the network.
336 |
337 | Corresponding Source conveyed, and Installation Information provided,
338 | in accord with this section must be in a format that is publicly
339 | documented (and with an implementation available to the public in
340 | source code form), and must require no special password or key for
341 | unpacking, reading or copying.
342 |
343 | 7. Additional Terms.
344 |
345 | "Additional permissions" are terms that supplement the terms of this
346 | License by making exceptions from one or more of its conditions.
347 | Additional permissions that are applicable to the entire Program shall
348 | be treated as though they were included in this License, to the extent
349 | that they are valid under applicable law. If additional permissions
350 | apply only to part of the Program, that part may be used separately
351 | under those permissions, but the entire Program remains governed by
352 | this License without regard to the additional permissions.
353 |
354 | When you convey a copy of a covered work, you may at your option
355 | remove any additional permissions from that copy, or from any part of
356 | it. (Additional permissions may be written to require their own
357 | removal in certain cases when you modify the work.) You may place
358 | additional permissions on material, added by you to a covered work,
359 | for which you have or can give appropriate copyright permission.
360 |
361 | Notwithstanding any other provision of this License, for material you
362 | add to a covered work, you may (if authorized by the copyright holders of
363 | that material) supplement the terms of this License with terms:
364 |
365 | a) Disclaiming warranty or limiting liability differently from the
366 | terms of sections 15 and 16 of this License; or
367 |
368 | b) Requiring preservation of specified reasonable legal notices or
369 | author attributions in that material or in the Appropriate Legal
370 | Notices displayed by works containing it; or
371 |
372 | c) Prohibiting misrepresentation of the origin of that material, or
373 | requiring that modified versions of such material be marked in
374 | reasonable ways as different from the original version; or
375 |
376 | d) Limiting the use for publicity purposes of names of licensors or
377 | authors of the material; or
378 |
379 | e) Declining to grant rights under trademark law for use of some
380 | trade names, trademarks, or service marks; or
381 |
382 | f) Requiring indemnification of licensors and authors of that
383 | material by anyone who conveys the material (or modified versions of
384 | it) with contractual assumptions of liability to the recipient, for
385 | any liability that these contractual assumptions directly impose on
386 | those licensors and authors.
387 |
388 | All other non-permissive additional terms are considered "further
389 | restrictions" within the meaning of section 10. If the Program as you
390 | received it, or any part of it, contains a notice stating that it is
391 | governed by this License along with a term that is a further
392 | restriction, you may remove that term. If a license document contains
393 | a further restriction but permits relicensing or conveying under this
394 | License, you may add to a covered work material governed by the terms
395 | of that license document, provided that the further restriction does
396 | not survive such relicensing or conveying.
397 |
398 | If you add terms to a covered work in accord with this section, you
399 | must place, in the relevant source files, a statement of the
400 | additional terms that apply to those files, or a notice indicating
401 | where to find the applicable terms.
402 |
403 | Additional terms, permissive or non-permissive, may be stated in the
404 | form of a separately written license, or stated as exceptions;
405 | the above requirements apply either way.
406 |
407 | 8. Termination.
408 |
409 | You may not propagate or modify a covered work except as expressly
410 | provided under this License. Any attempt otherwise to propagate or
411 | modify it is void, and will automatically terminate your rights under
412 | this License (including any patent licenses granted under the third
413 | paragraph of section 11).
414 |
415 | However, if you cease all violation of this License, then your
416 | license from a particular copyright holder is reinstated (a)
417 | provisionally, unless and until the copyright holder explicitly and
418 | finally terminates your license, and (b) permanently, if the copyright
419 | holder fails to notify you of the violation by some reasonable means
420 | prior to 60 days after the cessation.
421 |
422 | Moreover, your license from a particular copyright holder is
423 | reinstated permanently if the copyright holder notifies you of the
424 | violation by some reasonable means, this is the first time you have
425 | received notice of violation of this License (for any work) from that
426 | copyright holder, and you cure the violation prior to 30 days after
427 | your receipt of the notice.
428 |
429 | Termination of your rights under this section does not terminate the
430 | licenses of parties who have received copies or rights from you under
431 | this License. If your rights have been terminated and not permanently
432 | reinstated, you do not qualify to receive new licenses for the same
433 | material under section 10.
434 |
435 | 9. Acceptance Not Required for Having Copies.
436 |
437 | You are not required to accept this License in order to receive or
438 | run a copy of the Program. Ancillary propagation of a covered work
439 | occurring solely as a consequence of using peer-to-peer transmission
440 | to receive a copy likewise does not require acceptance. However,
441 | nothing other than this License grants you permission to propagate or
442 | modify any covered work. These actions infringe copyright if you do
443 | not accept this License. Therefore, by modifying or propagating a
444 | covered work, you indicate your acceptance of this License to do so.
445 |
446 | 10. Automatic Licensing of Downstream Recipients.
447 |
448 | Each time you convey a covered work, the recipient automatically
449 | receives a license from the original licensors, to run, modify and
450 | propagate that work, subject to this License. You are not responsible
451 | for enforcing compliance by third parties with this License.
452 |
453 | An "entity transaction" is a transaction transferring control of an
454 | organization, or substantially all assets of one, or subdividing an
455 | organization, or merging organizations. If propagation of a covered
456 | work results from an entity transaction, each party to that
457 | transaction who receives a copy of the work also receives whatever
458 | licenses to the work the party's predecessor in interest had or could
459 | give under the previous paragraph, plus a right to possession of the
460 | Corresponding Source of the work from the predecessor in interest, if
461 | the predecessor has it or can get it with reasonable efforts.
462 |
463 | You may not impose any further restrictions on the exercise of the
464 | rights granted or affirmed under this License. For example, you may
465 | not impose a license fee, royalty, or other charge for exercise of
466 | rights granted under this License, and you may not initiate litigation
467 | (including a cross-claim or counterclaim in a lawsuit) alleging that
468 | any patent claim is infringed by making, using, selling, offering for
469 | sale, or importing the Program or any portion of it.
470 |
471 | 11. Patents.
472 |
473 | A "contributor" is a copyright holder who authorizes use under this
474 | License of the Program or a work on which the Program is based. The
475 | work thus licensed is called the contributor's "contributor version".
476 |
477 | A contributor's "essential patent claims" are all patent claims
478 | owned or controlled by the contributor, whether already acquired or
479 | hereafter acquired, that would be infringed by some manner, permitted
480 | by this License, of making, using, or selling its contributor version,
481 | but do not include claims that would be infringed only as a
482 | consequence of further modification of the contributor version. For
483 | purposes of this definition, "control" includes the right to grant
484 | patent sublicenses in a manner consistent with the requirements of
485 | this License.
486 |
487 | Each contributor grants you a non-exclusive, worldwide, royalty-free
488 | patent license under the contributor's essential patent claims, to
489 | make, use, sell, offer for sale, import and otherwise run, modify and
490 | propagate the contents of its contributor version.
491 |
492 | In the following three paragraphs, a "patent license" is any express
493 | agreement or commitment, however denominated, not to enforce a patent
494 | (such as an express permission to practice a patent or covenant not to
495 | sue for patent infringement). To "grant" such a patent license to a
496 | party means to make such an agreement or commitment not to enforce a
497 | patent against the party.
498 |
499 | If you convey a covered work, knowingly relying on a patent license,
500 | and the Corresponding Source of the work is not available for anyone
501 | to copy, free of charge and under the terms of this License, through a
502 | publicly available network server or other readily accessible means,
503 | then you must either (1) cause the Corresponding Source to be so
504 | available, or (2) arrange to deprive yourself of the benefit of the
505 | patent license for this particular work, or (3) arrange, in a manner
506 | consistent with the requirements of this License, to extend the patent
507 | license to downstream recipients. "Knowingly relying" means you have
508 | actual knowledge that, but for the patent license, your conveying the
509 | covered work in a country, or your recipient's use of the covered work
510 | in a country, would infringe one or more identifiable patents in that
511 | country that you have reason to believe are valid.
512 |
513 | If, pursuant to or in connection with a single transaction or
514 | arrangement, you convey, or propagate by procuring conveyance of, a
515 | covered work, and grant a patent license to some of the parties
516 | receiving the covered work authorizing them to use, propagate, modify
517 | or convey a specific copy of the covered work, then the patent license
518 | you grant is automatically extended to all recipients of the covered
519 | work and works based on it.
520 |
521 | A patent license is "discriminatory" if it does not include within
522 | the scope of its coverage, prohibits the exercise of, or is
523 | conditioned on the non-exercise of one or more of the rights that are
524 | specifically granted under this License. You may not convey a covered
525 | work if you are a party to an arrangement with a third party that is
526 | in the business of distributing software, under which you make payment
527 | to the third party based on the extent of your activity of conveying
528 | the work, and under which the third party grants, to any of the
529 | parties who would receive the covered work from you, a discriminatory
530 | patent license (a) in connection with copies of the covered work
531 | conveyed by you (or copies made from those copies), or (b) primarily
532 | for and in connection with specific products or compilations that
533 | contain the covered work, unless you entered into that arrangement,
534 | or that patent license was granted, prior to 28 March 2007.
535 |
536 | Nothing in this License shall be construed as excluding or limiting
537 | any implied license or other defenses to infringement that may
538 | otherwise be available to you under applicable patent law.
539 |
540 | 12. No Surrender of Others' Freedom.
541 |
542 | If conditions are imposed on you (whether by court order, agreement or
543 | otherwise) that contradict the conditions of this License, they do not
544 | excuse you from the conditions of this License. If you cannot convey a
545 | covered work so as to satisfy simultaneously your obligations under this
546 | License and any other pertinent obligations, then as a consequence you may
547 | not convey it at all. For example, if you agree to terms that obligate you
548 | to collect a royalty for further conveying from those to whom you convey
549 | the Program, the only way you could satisfy both those terms and this
550 | License would be to refrain entirely from conveying the Program.
551 |
552 | 13. Use with the GNU Affero General Public License.
553 |
554 | Notwithstanding any other provision of this License, you have
555 | permission to link or combine any covered work with a work licensed
556 | under version 3 of the GNU Affero General Public License into a single
557 | combined work, and to convey the resulting work. The terms of this
558 | License will continue to apply to the part which is the covered work,
559 | but the special requirements of the GNU Affero General Public License,
560 | section 13, concerning interaction through a network will apply to the
561 | combination as such.
562 |
563 | 14. Revised Versions of this License.
564 |
565 | The Free Software Foundation may publish revised and/or new versions of
566 | the GNU General Public License from time to time. Such new versions will
567 | be similar in spirit to the present version, but may differ in detail to
568 | address new problems or concerns.
569 |
570 | Each version is given a distinguishing version number. If the
571 | Program specifies that a certain numbered version of the GNU General
572 | Public License "or any later version" applies to it, you have the
573 | option of following the terms and conditions either of that numbered
574 | version or of any later version published by the Free Software
575 | Foundation. If the Program does not specify a version number of the
576 | GNU General Public License, you may choose any version ever published
577 | by the Free Software Foundation.
578 |
579 | If the Program specifies that a proxy can decide which future
580 | versions of the GNU General Public License can be used, that proxy's
581 | public statement of acceptance of a version permanently authorizes you
582 | to choose that version for the Program.
583 |
584 | Later license versions may give you additional or different
585 | permissions. However, no additional obligations are imposed on any
586 | author or copyright holder as a result of your choosing to follow a
587 | later version.
588 |
589 | 15. Disclaimer of Warranty.
590 |
591 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
592 | APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
593 | HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
594 | OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
595 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
596 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
597 | IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
598 | ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
599 |
600 | 16. Limitation of Liability.
601 |
602 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
603 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
604 | THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
605 | GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
606 | USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
607 | DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
608 | PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
609 | EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
610 | SUCH DAMAGES.
611 |
612 | 17. Interpretation of Sections 15 and 16.
613 |
614 | If the disclaimer of warranty and limitation of liability provided
615 | above cannot be given local legal effect according to their terms,
616 | reviewing courts shall apply local law that most closely approximates
617 | an absolute waiver of all civil liability in connection with the
618 | Program, unless a warranty or assumption of liability accompanies a
619 | copy of the Program in return for a fee.
620 |
621 | END OF TERMS AND CONDITIONS
622 |
623 | How to Apply These Terms to Your New Programs
624 |
625 | If you develop a new program, and you want it to be of the greatest
626 | possible use to the public, the best way to achieve this is to make it
627 | free software which everyone can redistribute and change under these terms.
628 |
629 | To do so, attach the following notices to the program. It is safest
630 | to attach them to the start of each source file to most effectively
631 | state the exclusion of warranty; and each file should have at least
632 | the "copyright" line and a pointer to where the full notice is found.
633 |
634 |
635 | Copyright (C)
636 |
637 | This program is free software: you can redistribute it and/or modify
638 | it under the terms of the GNU General Public License as published by
639 | the Free Software Foundation, either version 3 of the License, or
640 | (at your option) any later version.
641 |
642 | This program is distributed in the hope that it will be useful,
643 | but WITHOUT ANY WARRANTY; without even the implied warranty of
644 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
645 | GNU General Public License for more details.
646 |
647 | You should have received a copy of the GNU General Public License
648 | along with this program. If not, see .
649 |
650 | Also add information on how to contact you by electronic and paper mail.
651 |
652 | If the program does terminal interaction, make it output a short
653 | notice like this when it starts in an interactive mode:
654 |
655 | Copyright (C)
656 | This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
657 | This is free software, and you are welcome to redistribute it
658 | under certain conditions; type `show c' for details.
659 |
660 | The hypothetical commands `show w' and `show c' should show the appropriate
661 | parts of the General Public License. Of course, your program's commands
662 | might be different; for a GUI interface, you would use an "about box".
663 |
664 | You should also get your employer (if you work as a programmer) or school,
665 | if any, to sign a "copyright disclaimer" for the program, if necessary.
666 | For more information on this, and how to apply and follow the GNU GPL, see
667 | .
668 |
669 | The GNU General Public License does not permit incorporating your program
670 | into proprietary programs. If your program is a subroutine library, you
671 | may consider it more useful to permit linking proprietary applications with
672 | the library. If this is what you want to do, use the GNU Lesser General
673 | Public License instead of this License. But first, please read
674 | .
675 |
--------------------------------------------------------------------------------
/CastleEngineManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
14 |
15 |
20 |
21 |
22 |
23 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Castle Game Engine Pascal Language Server
2 |
3 | This is an [LSP](https://microsoft.github.io/language-server-protocol/) for Pascal using [CodeTools](https://wiki.lazarus.freepascal.org/Codetools) from Lazarus under the hood. In simple terms: this application can provide code completion features, for Pascal code, to all text editors that may be _LSP clients_ (like _VS Code_, _Emacs_, _NeoVim_).
4 |
5 | Distributed with [Castle Game Engine](https://castle-engine.io/). [Download](https://castle-engine.io/download) and install the engine and you will have the `bin/pasls` binary in the engine directory.
6 |
7 | Cooperates with [Castle Game Engine VS Code extension](https://marketplace.visualstudio.com/items?itemName=castle-engine-team.castle-engine). Our VS Code extension can be used to build and run CGE projects, and it can integrate with this LSP server.
8 |
9 | ## History
10 |
11 | We are fork of [Philip Zander LSP Pascal server](https://github.com/Isopod/pascal-language-server/).
12 |
13 | Which was in turm forked from https://github.com/arjanadriaanse/pascal-language-server , but has since been mostly rewritten.
14 |
15 | CGE fork contributes back improvements that are not CGE-specific (see e.g. https://github.com/Isopod/pascal-language-server/pull/1 , https://github.com/Isopod/pascal-language-server/pull/2 , https://github.com/Isopod/pascal-language-server/pull/4 ).
16 |
17 | ## Castle Game Engine LSP server improvements
18 |
19 | - Works with [Castle Game Engine VS Code extension](https://marketplace.visualstudio.com/items?itemName=castle-engine-team.castle-engine) out-of-the-box. In this case, you should not need to use `castle-pasls.ini` -- just configure everything using VS Code extension settings.
20 |
21 | - We add capability to configure the LSP server using `castle-pasls.ini` to:
22 | - Define _Castle Game Engine_ path that will make `pasls` aware of CGE units and autocomplete CGE API.
23 | - Add extra FPC options.
24 | - Provide custom Lazarus config location (useful if you install Lazarus by [fpcupdeluxe](https://castle-engine.io/fpcupdeluxe) but still want `pasls` to read Lazarus config -- this is optional).
25 | - Improve debugging by known log filename and more complete JSON logs.
26 |
27 | - We can also auto-detect _Castle Game Engine_ path in some situations:
28 | - If the LSP server binary is distributed in `bin` of _Castle Game Engine_.
29 | - Or if the environment 'CASTLE_ENGINE_PATH` is defined.
30 | - Or if you're on Unix and using `/usr/src/castle-engine/` or `/usr/local/src/castle-engine/`.
31 |
32 | - We also pass (to code completion engine) _Castle Game Engine_ options that are also passed by [CGE build tool](https://castle-engine.io/build_tool) like `-Mobjfpc -Sm -Sc -Sg -Si -Sh`.
33 |
34 | - We autodetect OS and CPU harder, and we fix OS=`windows` to proper `win64` or `win32` (common mistake, esp. because of https://github.com/genericptr/pasls-vscode/issues/1 ).
35 |
36 | ## Features
37 |
38 | - Code completion
39 | - Signature help
40 | - Go to declaration
41 | - Go to definition
42 | - Automatic dependency resolution for `.lpk` and `.lpr` files
43 | - Works with include files, as long as they specify `{%MainUnit xxx.pas}` at the top (just like for Lazarus CodeTools)
44 | - Detection of _Castle Game Engine_ unit paths in a various ways
45 |
46 | ## Building
47 |
48 | First, make sure, submodules are loaded:
49 | ```
50 | git submodule update --init --recursive
51 | ```
52 |
53 | To compile, open the project file in Lazarus or use the command line:
54 |
55 | ```sh
56 | cd server
57 | lazbuild pasls.lpi
58 | ```
59 |
60 | It is recommended to use Free Pascal Compiler version 3.2.0 and Lazarus version
61 | 2.0.8 or later, older versions are not officially supported.
62 |
63 | ## Clients
64 |
65 | ### Neovim ≥ 0.5.0
66 |
67 | For information on how to use the server from Neovim, see [client/nvim](client/nvim).
68 |
69 | ### Emacs
70 |
71 | To use the server from `lsp-mode` in Emacs, install the separate
72 | [`lsp-pascal`](https://github.com/arjanadriaanse/lsp-pascal) module.
73 | Full example setup of it is documented in [Michalis notes about LSP + Pascal](https://github.com/michaliskambi/elisp/tree/master/lsp).
74 |
75 | ### VS Code
76 |
77 | Install the [Castle Game Engine VS Code extension](https://marketplace.visualstudio.com/items?itemName=castle-engine-team.castle-engine).
78 |
79 | ### Other
80 | Any editor that allows you to add custom LSP configurations should work.
81 |
82 | ## Configuration
83 |
84 | In order for the language server to find all the units, it needs to know the
85 | following parameters:
86 |
87 | - location of the FPC standard library source files
88 | - location of the FPC compiler executable
89 | - location of the Lazarus install directory
90 | - the OS you are compiling for
91 | - the architecture you are compiling for
92 |
93 | By default, the server will try to auto-detect these parameters from your
94 | Lazarus config. It will search for config files in the following locations (the
95 | exact paths will depend on your operating system):
96 |
97 | - `/lazarus` (e.g. `/home/user/.config/lazarus`)
98 | - `/.lazarus` (e.g. `/home/user/.lazarus`)
99 | - `/lazarus` (e.g. `/etc/lazarus`)
100 | - Custom directory specified in `castle-pasls.ini` as `config` in `[lazarus]` section (see below for example). This is useful in case your Lazarus config is in a special directory, as e.g. usually setup by fpcupdeluxe.
101 |
102 | In addition, you can also specify these parameters manually in one of the
103 | following ways:
104 |
105 | 1. Set the environment variables:
106 |
107 | - `PP` — Path to the FPC compiler executable
108 | - `FPCDIR` — Path of the source code of the FPC standard library
109 | - `LAZARUSDIR` — Path of your Lazarus installation
110 | - `FPCTARGET` — Target OS (e.g. Linux, Darwin, ...)
111 | - `FPCTARGETCPU` — Target architecture (e.g. x86_64, AARCH64, ...)
112 |
113 | This overrides auto-detected settings.
114 |
115 | 2. Or specify the locations via LSP `initializationOptions`. How this is done
116 | will depend on your client. The format is the following:
117 | ```json
118 | {
119 | "PP": "",
120 | "FPCDIR": "",
121 | "LAZARUSDIR": "",
122 | "FPCTARGET": "",
123 | "FPCTARGETCPU": ""
124 | }
125 | ```
126 |
127 | This overrides environment variables.
128 |
129 | ## Extra configuration in LSP initialization options
130 |
131 | Additional keys in LSP initialization options can be used to influence the LSP server behavior. See the docs of your LSP client (text editor) to know how to pass initialization options.
132 |
133 | - `syntaxErrorReportingMode` (integer): Determines how to report syntax errors. Syntax errors indicate that CodeTools cannot understand the surrounding Pascal code well enough to provide any code completion.
134 |
135 | - 0 (default): Show an error message. This relies on the LSP client (text editor) handling the `window/showMessage` message. Support in various text editor:
136 |
137 | - VS Code: works.
138 |
139 | - NeoVim (0.8.0): works, the message is shown for ~1 sec by default.
140 |
141 | - Emacs: works, the message is visible in [echo area](https://www.emacswiki.org/emacs/EchoArea) and the `*Messages*` buffer. You can filter out useless `No completion found` messages to make it perfect, see https://github.com/michaliskambi/elisp/blob/master/lsp/kambi-pascal-lsp.el for example.
142 |
143 | - 1: Return a fake completion item with the error message. This works well in VC Code and NeoVim -- while the completion item doesn't really complete anything, but the error message is clearly visible.
144 |
145 | - 2: Return an error to the LSP client. Some LSP clients will just hide the error, but some (like Emacs) will show it clearly and prominently.
146 |
147 | ## Extra configuration in castle-engine/pascal-language-server
148 |
149 | The `pasls` reads configuration file `castle-pasls.ini` in user config dir to enable some additional features.
150 |
151 | Where exactly is the config file?
152 |
153 | - On Unix: `$HOME/.config/pasls/castle-pasls.ini`
154 | - On Windows: `C:/Users//AppData/Local/pasls/castle-pasls.ini`
155 | - In general: Uncomment `WriteLn('Reading config from ', FileName);` in `server/castlelsp.pas`, run `pasls` manually, see the output.
156 |
157 | Allowed options:
158 |
159 | ```
160 | [log]
161 | ;; Where to write log (contains DebugLog output, allows to debug how everything in pasls behaves).
162 | ;; We will add suffix with process id, like '.pid123' .
163 | ;; By default none.
164 | filename=/tmp/pasls-log.txt
165 |
166 | ;; Whether to dump full JSON request/response contents to log (may be quite long).
167 | ;; By default this is false (0), and JSON request/response logs are cut at 2000 characters.
168 | ;; You change it to true (1) to have full logs, useful at debugging.
169 | full_json=1
170 |
171 | [lazarus]
172 | ;; Custom directory with Lazarus config.
173 | ;; It should contain files like environmentoptions.xml, fpcdefines.xml .
174 | ;; See the log output to know if pasls read successfully XML files from there.
175 | config=/home/michalis/installed/fpclazarus/current/config_lazarus/
176 |
177 | [castle]
178 | ;; Castle Game Engine location.
179 | ;;
180 | ;; Set this to make pasls autocomplete CGE API by:
181 | ;; 1. knowing paths to all CGE units (derived from this CGE path),
182 | ;; 2. using default CGE compilation settings, like -Mobjfpc and -Sh (used by CGE build tool and editor).
183 | ;;
184 | ;; NOTE: Instead of setting the engine path here,
185 | ;; VS Code users should configure "Engine Path" in the VS Code extension settings.
186 | path=/home/michalis/sources/castle-engine/castle-engine/
187 |
188 | [extra_options]
189 | ;; Specify as many extra FPC options as you want.
190 | ;; Each extra option must have a consecutive number, we start from 1, and stop when
191 | ;; an option does not exist (or is an empty string).
192 | option_1=-Fu/home/michalis/sources/castle-engine/castle-engine/tests/code/tester-fpcunit
193 | option_2=-dSOME_DEFINE
194 | option_3=-dSOMETHING_MORE
195 | ```
196 |
197 | ## Roadmap
198 |
199 | ### Wishlist
200 |
201 | - Renaming of identifiers
202 | - “Find all references”
203 | - Signature help: Highlight active parameter
204 | - Code formatting?
205 |
206 | ### Known bugs
207 |
208 | - Signature help does not show all overloads
209 |
--------------------------------------------------------------------------------
/client/nvim/README.md:
--------------------------------------------------------------------------------
1 | ## Using Pascal Language Server in Neovim
2 |
3 | Assuming you have already compiled the server. If you haven't, do that first.
4 |
5 | - Install [nvim-lspconfig](https://github.com/neovim/nvim-lspconfig)
6 | - Copy `pascal.lua` into `~/.local/share/nvim/site/pack/vendor/opt/nvim-lspconfig/lua/lspconfig/`
7 | (yes, quite a mouthful)
8 | - Make sure `pasls` is in `PATH` (or edit `pascal.lua` to use an absolute path
9 | pointing to the executable)
10 | - Enable `pasls` in your `init.vim`:
11 | ```vim
12 | packadd nvim-lspconfig
13 | lua require'lspconfig'.pascal.setup{}
14 | ```
15 |
16 | Note that this will not by itself enable any features as those are managed by
17 | separate plugins in Neovim.
18 |
19 | ## Example config
20 | The following example uses
21 | [completion-nvim](https://github.com/nvim-lua/completion-nvim) for code
22 | completion.
23 |
24 | ```vim
25 | packadd nvim-lspconfig
26 | packadd completion-nvim
27 |
28 | lua << EOF
29 | local nvim_lsp = require('lspconfig')
30 |
31 | local on_attach = function(client, bufnr)
32 | local function buf_set_keymap(...) vim.api.nvim_buf_set_keymap(bufnr, ...) end
33 | local function buf_set_option(...) vim.api.nvim_buf_set_option(bufnr, ...) end
34 |
35 | require'completion'.on_attach(client, bufnr)
36 |
37 | -- Enable completion triggered by
38 | buf_set_option('omnifunc', 'v:lua.vim.lsp.omnifunc')
39 |
40 | -- Mappings.
41 | local opts = { noremap=true, silent=true }
42 | buf_set_keymap('n', 'gD', 'lua vim.lsp.buf.declaration()', opts)
43 | buf_set_keymap('n', 'gd', 'lua vim.lsp.buf.definition()', opts)
44 | end
45 |
46 | require'lspconfig'.fpc.setup{on_attach=on_attach}
47 |
48 | " Make code completion less intrusive
49 | let g:completion_enable_auto_popup = 0
50 | let g:completion_enable_auto_signature = 1
51 |
52 | " Invoke code completion with +
53 | imap (completion_trigger)
54 |
55 | EOF
56 | ```
57 |
58 | ## Compatibility
59 |
60 | LSP is included in Neovim 0.5.0+
61 |
--------------------------------------------------------------------------------
/client/nvim/pascal.lua:
--------------------------------------------------------------------------------
1 | local configs = require 'lspconfig/configs'
2 | local util = require 'lspconfig/util'
3 |
4 | configs.pascal = {
5 | default_config = {
6 | cmd = {
7 | "pasls",
8 | -- Uncomment for debugging:
9 | --"--save-log", "pasls-log", "--save-replay", "pasls-replay"
10 | };
11 | filetypes = {"pascal"};
12 | root_dir = util.root_pattern(".git", "Makefile.fpc");
13 | init_options = {}
14 | };
15 | docs = {
16 | description = [[
17 | https://github.com/Isopod/pascal-language-server
18 |
19 | `pascal-language-server`, a language server for Pascal, based on fpc.
20 | ]];
21 | default_config = {
22 | root_dir = [[root_pattern(".git", "Makefile.fpc")]];
23 | };
24 | };
25 | };
26 |
27 | -- vim:et ts=2 sw=2
28 |
--------------------------------------------------------------------------------
/server/castlearchitectures.pas:
--------------------------------------------------------------------------------
1 | {
2 | Copyright 2022-2023 Michalis Kamburelis
3 |
4 | Extensions to the Pascal Language Server specific to Castle Game Engine.
5 |
6 | Distributed on permissive "modified BSD 3-clause",
7 | https://github.com/castle-engine/castle-engine/blob/master/doc/licenses/COPYING.BSD-3-clause.txt ,
8 | so that it can be combined with any other licenses without issues. }
9 |
10 | { Autodetect OS and CPU.
11 | Adjusted from CGE ToolArchitectures unit,
12 | which in turn is based on FPC FPMkUnit. }
13 | unit CastleArchitectures;
14 |
15 | interface
16 |
17 | function AutoDetectOS: String;
18 | function AutoDetectCPU: String;
19 |
20 | implementation
21 |
22 | uses TypInfo;
23 |
24 | type
25 | { Processor architectures supported by FPC. Copied from FPMkUnit. }
26 | TCpu=(cpuNone,
27 | i386,m68k,powerpc,sparc,x86_64,arm,powerpc64,avr,armeb,
28 | mips,mipsel,jvm,i8086,aarch64,sparc64
29 | );
30 |
31 | { Operating systems supported by FPC. Copied from FPMkUnit. }
32 | TOS=(osNone,
33 | linux,go32v2,win32,os2,freebsd,beos,netbsd,
34 | amiga,atari, solaris, qnx, netware, openbsd,wdosx,
35 | palmos,macosclassic,darwin,emx,watcom,morphos,netwlibc,
36 | win64,wince,gba,nds,embedded,symbian,haiku,iphonesim,
37 | aix,java,android,nativent,msdos,wii,aros,dragonfly,
38 | win16,ios
39 | );
40 |
41 | const
42 | DefaultCPU: TCPU =
43 | {$ifdef CPUi386} i386 {$endif}
44 | {$ifdef CPUm68k} m68k {$endif}
45 | {$ifdef CPUpowerpc32} powerpc {$endif}
46 | {$ifdef CPUsparc} sparc {$endif}
47 | {$ifdef CPUx86_64} x86_64 {$endif}
48 | {$ifdef CPUarm} arm {$endif}
49 | {$ifdef CPUaarch64} aarch64 {$endif}
50 | {$ifdef CPUpowerpc64} powerpc64 {$endif}
51 | {$ifdef CPUavr} avr {$endif}
52 | {$ifdef CPUarmeb} armeb {$endif}
53 | {$ifdef CPUmips} mips {$endif}
54 | {$ifdef CPUmipsel} mipsel {$endif}
55 | {$ifdef CPUjvm} jvm {$endif}
56 | {$ifdef CPUi8086} i8086 {$endif}
57 | {$ifdef CPUsparc64} sparc64 {$endif}
58 | ;
59 |
60 | DefaultOS: TOS =
61 | {$ifdef linux} linux {$endif}
62 | {$ifdef go32v2} go32v2 {$endif}
63 | {$ifdef win32} win32 {$endif}
64 | {$ifdef os2} os2 {$endif}
65 | {$ifdef freebsd} freebsd {$endif}
66 | {$ifdef beos} beos {$endif}
67 | {$ifdef netbsd} netbsd {$endif}
68 | {$ifdef amiga} amiga {$endif}
69 | {$ifdef atari} atari {$endif}
70 | {$ifdef solaris} solaris {$endif}
71 | {$ifdef qnx} qnx {$endif}
72 | {$ifdef netware} netware {$endif}
73 | {$ifdef openbsd} openbsd {$endif}
74 | {$ifdef wdosx} wdosx {$endif}
75 | {$ifdef palmos} palmos {$endif}
76 | {$ifdef macosclassic} macosclassic {$endif} // TODO: what is symbol of this? It used to be macos?
77 | {$ifdef darwin} darwin {$endif}
78 | {$ifdef emx} emx {$endif}
79 | {$ifdef watcom} watcom {$endif}
80 | {$ifdef morphos} morphos {$endif}
81 | {$ifdef netwlibc} netwlibc {$endif}
82 | {$ifdef win64} win64 {$endif}
83 | {$ifdef wince} wince {$endif}
84 | {$ifdef gba} gba {$endif}
85 | {$ifdef nds} nds {$endif}
86 | {$ifdef embedded} embedded {$endif}
87 | {$ifdef symbian} symbian {$endif}
88 | {$ifdef haiku} haiku {$endif}
89 | {$ifdef iphonesim} iphonesim {$endif}
90 | {$ifdef aix} aix {$endif}
91 | {$ifdef java} java {$endif}
92 | {$ifdef android} android {$endif}
93 | {$ifdef nativent} nativent {$endif}
94 | {$ifdef msdos} msdos {$endif}
95 | {$ifdef wii} wii {$endif}
96 | ;
97 |
98 | function CPUToString(CPU: TCPU): String;
99 | begin
100 | Result := LowerCase(GetEnumName(TypeInfo(TCPU), Ord(CPU)));
101 | end;
102 |
103 | function OSToString(OS: TOS): String;
104 | begin
105 | Result := LowerCase(GetEnumName(TypeInfo(TOS), Ord(OS)));
106 | end;
107 |
108 | function AutoDetectOS: String;
109 | begin
110 | Result := OSToString(DefaultOS);
111 | end;
112 |
113 | function AutoDetectCPU: String;
114 | begin
115 | Result := CPUToString(DefaultCPU);
116 | end;
117 |
118 | end.
119 |
--------------------------------------------------------------------------------
/server/castlelsp.pas:
--------------------------------------------------------------------------------
1 | {
2 | Copyright 2022-2024 Michalis Kamburelis
3 |
4 | Extensions to the Pascal Language Server specific to Castle Game Engine.
5 | See https://github.com/michaliskambi/elisp/tree/master/lsp
6 | about my notes about LSP + Pascal + Castle Game Engine + Emacs / VS Code.
7 | This file is reused with both forks:
8 |
9 | - Philip Zander (Isopod) fork
10 | original: https://github.com/Isopod/pascal-language-server
11 | CGE fork: https://github.com/castle-engine/pascal-language-server
12 |
13 | - Ryan Joseph (genericptr) fork
14 | original: https://github.com/genericptr/pascal-language-server
15 | CGE fork: https://github.com/michaliskambi/pascal-language-server-genericptr
16 |
17 | Distributed on permissive "modified BSD 3-clause",
18 | https://github.com/castle-engine/castle-engine/blob/master/doc/licenses/COPYING.BSD-3-clause.txt ,
19 | so that it can be combined with any other licenses without issues. }
20 |
21 | { Extensions to the Pascal Language Server specific to Castle Game Engine. }
22 | unit CastleLsp;
23 |
24 | interface
25 |
26 | uses Classes, IniFiles;
27 |
28 | var
29 | UserConfig: TIniFile;
30 | WorkspacePaths: TStringList; // paths to search by workspace symbols
31 | WorkspaceAndEnginePaths: TStringList; // paths to search by workspace symbols in engine developer mode
32 | EngineDeveloperMode: Boolean; // add engine paths to workspace symbols?
33 |
34 | procedure InitializeUserConfig;
35 |
36 | { Concatenated (by space) additional FPC options to pass to CodeTools.
37 |
38 | Contains:
39 | - extra CGE paths (derived from the single CGE path from castle-pasls.ini file)
40 | - extra CGE options (like -Mobjfpc)
41 | - extra free FPC options from castle-pasls.ini file
42 | }
43 | function ExtraFpcOptions: String;
44 |
45 | { Adds project search paths from manifest to workspace paths used by workspace symbols }
46 | procedure ParseWorkspacePaths(const ProjectSearchPaths, ProjectDirectory: String);
47 |
48 | implementation
49 |
50 | {$ifdef UNIX} {$define UNIX_WITH_USERS_UNIT} {$endif}
51 | { FPC 3.2.2 on Darwin doesn't contain Users. }
52 | {$ifdef DARWIN} {$undef UNIX_WITH_USERS_UNIT} {$endif}
53 |
54 | uses
55 | {$ifdef MSWINDOWS} Windows, {$endif}
56 | {$ifdef UNIX_WITH_USERS_UNIT} BaseUnix, {UnixUtils, - cannot be found, masked by UnixUtil?} Users, {$endif}
57 | SysUtils,
58 | UDebug;
59 |
60 | procedure InitializeUserConfig;
61 | var
62 | FileName: String;
63 | begin
64 | {$ifdef UNIX_WITH_USERS_UNIT}
65 | { Special hack for Unix + VSCode integration in https://github.com/genericptr/pasls-vscode ,
66 | looks like it overrides the environment and runs LSP server without $HOME defined,
67 | so GetAppConfigDir will not work (it will return relative path ".config/....". instead
68 | of proper absolute "/home/michalis/.config/....").
69 |
70 | Emacs LSP client doesn't have this problem. }
71 | if GetEnvironmentVariable('HOME') = '' then
72 | begin
73 | FileName := '/home/' + GetUserName(FpGetUID) + '/.config/pasls/castle-pasls.ini';
74 | end else
75 | {$endif}
76 | FileName := IncludeTrailingPathDelimiter(GetAppConfigDir(false)) + 'castle-pasls.ini';
77 |
78 | //WriteLn('Reading config from ', FileName);
79 | UserConfig := TIniFile.Create(FileName);
80 | end;
81 |
82 | { Detect Castle Game Engine path using various methods.
83 | Returns '' if cannot be detected, or absolute path (always ends with PathDelim)
84 | that was detected.
85 | The returned path, if non-empty, passed basic tests that
86 | it contains common CGE files/dirs. }
87 | function GetCastleEnginePath: String;
88 |
89 | { Check is Path a sensible CGE sources path.
90 | Requires Path to end with PathDelim. }
91 | function CheckCastlePath(const Path: String): Boolean;
92 | begin
93 | Result :=
94 | FileExists(Path + 'castle-fpc.cfg') and
95 | DirectoryExists(Path + 'src') and
96 | DirectoryExists(Path + 'tools' + PathDelim + 'build-tool' + PathDelim + 'data');
97 | end;
98 |
99 | function GetCastleEnginePathFromEnv: String;
100 | begin
101 | Result := GetEnvironmentVariable('CASTLE_ENGINE_PATH');
102 | if Result = '' then
103 | Exit;
104 |
105 | Result := IncludeTrailingPathDelimiter(Result);
106 | if CheckCastlePath(Result) then
107 | Exit;
108 |
109 | Result := '';
110 | end;
111 |
112 | function ExeName: String;
113 | {$if defined(LINUX)}
114 | var
115 | ExeLinkName: String;
116 | begin
117 | ExeLinkName := '/proc/' + IntToStr(FpGetpid) + '/exe';
118 | Result := FpReadLink(ExeLinkName);
119 | {$elseif defined(MSWINDOWS)}
120 | var
121 | S: UnicodeString;
122 | begin
123 | SetLength(S, MAX_PATH);
124 | if GetModuleFileNameW(0, PWideChar(@S[1]), MAX_PATH) = 0 then
125 | begin
126 | // WritelnWarning('GetModuleFileNameW failed. We will use old method to determine ExeName, which will fail if parent directory contains local characters');
127 | Exit(ParamStr(0)); // fallback to old method
128 | end;
129 | SetLength(S, StrLen(PWideChar(S))); // It's only null-terminated after WinAPI call, set actual length for Pascal UnicodeString
130 | Result := UTF8Encode(S);
131 | {$else}
132 | begin
133 | Result := ParamStr(0); // On non-Windows OSes, using ParamStr(0) for this is not reliable, but at least it's some default
134 | {$endif}
135 | end;
136 |
137 | function GetCastleEnginePathFromExeName: String;
138 | var
139 | ToolDir: String;
140 | begin
141 | ToolDir := ExtractFileDir(ExeName);
142 |
143 | { in case we're inside macOS bundle, use bundle path.
144 | This makes detection in case of CGE editor work OK. }
145 | {$ifdef DARWIN}
146 | // TODO: copy BundlePath from CGE? Or use CGE units here?
147 | // if BundlePath <> '' then
148 | // ToolDir := ExtractFileDir(ExclPathDelim(BundlePath));
149 | {$endif}
150 |
151 | { Check ../ of current exe, makes sense in released CGE version when
152 | tools are precompiled in bin/ subdirectory. }
153 | Result := IncludeTrailingPathDelimiter(ExtractFileDir(ToolDir));
154 | if CheckCastlePath(Result) then
155 | Exit;
156 | { Check ../../ of current exe, makes sense in development when
157 | each tool is compiled by various scripts in tools/xxx/ subdirectory. }
158 | Result := IncludeTrailingPathDelimiter(ExtractFileDir(ExtractFileDir(ToolDir)));
159 | if CheckCastlePath(Result) then
160 | Exit;
161 |
162 | Result := '';
163 | end;
164 |
165 | function GetCastleEnginePathSystemWide: String;
166 | begin
167 | {$ifdef UNIX}
168 | Result := '/usr/src/castle-engine/';
169 | if CheckCastlePath(Result) then
170 | Exit;
171 |
172 | Result := '/usr/local/src/castle-engine/';
173 | if CheckCastlePath(Result) then
174 | Exit;
175 | {$endif}
176 |
177 | Result := '';
178 | end;
179 |
180 | begin
181 | // use castle-pasls.ini
182 | Result := UserConfig.ReadString('castle', 'path', '');
183 | // try to find CGE on $CASTLE_ENGINE_PATH
184 | if Result = '' then
185 | Result := GetCastleEnginePathFromEnv;
186 | // try to find CGE on path relative to current exe
187 | if Result = '' then
188 | Result := GetCastleEnginePathFromExeName;
189 | // try to find CGE on system-wide paths
190 | if Result = '' then
191 | Result := GetCastleEnginePathSystemWide;
192 | end;
193 |
194 | function ExtraFpcOptions: String;
195 |
196 | { Quote arguments passed to FPC in case they contain spaces.
197 |
198 | There's no cleaner way unfortunately: parameters of CodeTools API,
199 | like FPCOptions, are taken as a single string, with all parameters glued
200 | by a space.
201 | (it would be cleaned if CodeTools API would be changed to take TStringList.)
202 |
203 | So we have to quote parameters that contain spaces.
204 | We add " around, which seems to work with FPC 3.2.2. }
205 | function QuoteFpcOption(const S: String): String;
206 | begin
207 | if Pos(' ', S) <> 0 then
208 | begin
209 | if Pos('"', S) <> 0 then
210 | DebugLog(' WARNING: Parameter "%s" contains both spaces and double quotes, cannot quote it reliably for FPC', [S]);
211 | Result := '"' + S + '"';
212 | end else
213 | Result := S;
214 | end;
215 |
216 | function CastleOptionsFromCfg(CastleEnginePath: String): String;
217 | var
218 | CastleFpcCfg: TStringList;
219 | S, UntrimmedS: String;
220 | begin
221 | CastleEnginePath := IncludeTrailingPathDelimiter(CastleEnginePath);
222 | Result := '';
223 |
224 | CastleFpcCfg := TStringList.Create;
225 | try
226 | CastleFpcCfg.LoadFromFile(CastleEnginePath + 'castle-fpc.cfg');
227 | for UntrimmedS in CastleFpcCfg do
228 | begin
229 | S := Trim(UntrimmedS);
230 | if S.Startswith('-Fu', true) or
231 | S.Startswith('-Fi', true) then
232 | begin
233 | Insert(CastleEnginePath, S, 4);
234 | Result := Result + ' ' + QuoteFpcOption(S);
235 | end;
236 | end;
237 | finally FreeAndNil(CastleFpcCfg) end;
238 | end;
239 |
240 | const
241 | { Add the same syntax options as are specified by CGE build tool in
242 | castle-engine/tools/build-tool/code/toolcompile.pas .
243 |
244 | This is necessary to allow pasls to understand Pascal units that don't include
245 | castleconf.inc but still rely in CGE Pascal configuration, which means:
246 | all example and applications.
247 | E.g. examples/fps_game/code/gameenemy.pas uses generics and relies on ObjFpc mode. }
248 | CastleOtherOptions = ' -Mobjfpc -Sm -Sc -Sg -Si -Sh';
249 | var
250 | CastleEnginePath, ExtraOption: String;
251 | ExtraOptionIndex: Integer;
252 | begin
253 | Result := CastleOtherOptions;
254 |
255 | CastleEnginePath := GetCastleEnginePath;
256 | if CastleEnginePath <> '' then
257 | begin
258 | DebugLog(' Castle Game Engine path detected: %s', [CastleEnginePath]);
259 | Result := Result + CastleOptionsFromCfg(CastleEnginePath);
260 | end else
261 | begin
262 | DebugLog(' WARNING: Castle Game Engine path not detected, completion of CGE API will not work.', []);
263 | end;
264 |
265 | ExtraOptionIndex := 1;
266 | while true do
267 | begin
268 | ExtraOption := UserConfig.ReadString('extra_options', 'option_' + IntToStr(ExtraOptionIndex), '');
269 | if ExtraOption = '' then
270 | Break;
271 | Inc(ExtraOptionIndex);
272 | Result := Result + ' ' + QuoteFpcOption(ExtraOption);
273 | end;
274 | end;
275 |
276 |
277 | procedure ParseWorkspacePaths(const ProjectSearchPaths, ProjectDirectory: String);
278 |
279 | { Parse castle-fpc.cfg for units paths.
280 | Adds absolute paths (beginning with CastleEnginePath) to UnitsPaths.
281 | CastleEnginePath cannot be '' when calling this function. }
282 | procedure AddEngineUnitsPathsFromCfg(const CastleEnginePath: String; const UnitsPaths: TStrings);
283 | var
284 | CastleFpcCfg: TStringList;
285 | UntrimmedS, S: String;
286 | begin
287 | Assert(CastleEnginePath <> '');
288 | CastleFpcCfg := TStringList.Create;
289 | try
290 | CastleFpcCfg.LoadFromFile(CastleEnginePath + 'castle-fpc.cfg');
291 | for UntrimmedS in CastleFpcCfg do
292 | begin
293 | S := Trim(UntrimmedS);
294 | { Note that we look at units paths (-Fu) and ignore include paths (-Fi).
295 | Reason: The output of this is used with WorkspaceSymbol,
296 | that only uses these paths to scan for .pas files (units) anyway.
297 | Besides, in case of castle-fpc.cfg, the -Fi mostly duplicate -Fu anyway. }
298 | if S.Startswith('-Fu', true) then
299 | begin
300 | Delete(S, 1, 3);
301 | UnitsPaths.Add(CastleEnginePath + S);
302 | end;
303 | end;
304 | finally FreeAndNil(CastleFpcCfg) end;
305 | end;
306 |
307 | var
308 | I: Integer;
309 | CastleEnginePath: String;
310 | begin
311 | if Trim(ProjectSearchPaths) = '' then
312 | Exit;
313 |
314 | WorkspacePaths.Text := ProjectSearchPaths;
315 | for I := 0 to WorkspacePaths.Count -1 do
316 | WorkspacePaths[I] := IncludeTrailingPathDelimiter(ProjectDirectory) + WorkspacePaths[I];
317 |
318 | WorkspacePaths.Insert(0, ProjectDirectory);
319 |
320 | WorkspaceAndEnginePaths.Text := WorkspacePaths.Text;
321 |
322 | CastleEnginePath := GetCastleEnginePath;
323 | if CastleEnginePath <> '' then
324 | begin
325 | AddEngineUnitsPathsFromCfg(CastleEnginePath, WorkspaceAndEnginePaths);
326 | end else
327 | begin
328 | DebugLog(' WARNING: Castle Game Engine path not detected, jumping to CGE symbols (in "Engine Developer Mode") will not work.', []);
329 | end;
330 | end;
331 |
332 | initialization
333 | WorkspacePaths := TStringList.Create;
334 | WorkspaceAndEnginePaths := TStringList.Create;
335 |
336 | finalization
337 | FreeAndNil(WorkspaceAndEnginePaths);
338 | FreeAndNil(WorkspacePaths);
339 | FreeAndNil(UserConfig);
340 | end.
341 |
--------------------------------------------------------------------------------
/server/pasls.lpi:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 |
105 |
106 |
107 |
108 |
109 |
110 |
111 |
112 |
113 |
114 |
115 |
116 |
117 |
118 |
119 |
120 |
121 |
122 |
123 |
124 |
125 |
126 |
127 |
128 |
129 |
130 |
131 |
132 |
133 |
134 |
135 |
136 |
137 |
138 |
139 |
140 |
141 |
142 |
143 |
144 |
145 |
146 |
147 |
148 |
149 |
150 |
151 |
152 |
153 |
154 |
155 |
156 |
157 |
158 |
159 |
160 |
161 |
162 |
163 |
164 |
165 |
166 |
167 |
168 |
--------------------------------------------------------------------------------
/server/pasls.lpr:
--------------------------------------------------------------------------------
1 | // Pascal Language Server
2 | // Copyright 2020 Arjan Adriaanse
3 | // 2021 Philip Zander
4 |
5 | // This file is part of Pascal Language Server.
6 |
7 | // Pascal Language Server is free software: you can redistribute it
8 | // and/or modify it under the terms of the GNU General Public License
9 | // as published by the Free Software Foundation, either version 3 of
10 | // the License, or (at your option) any later version.
11 |
12 | // Pascal Language Server is distributed in the hope that it will be
13 | // useful, but WITHOUT ANY WARRANTY; without even the implied warranty
14 | // of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 | // GNU General Public License for more details.
16 |
17 | // You should have received a copy of the GNU General Public License
18 | // along with Pascal Language Server. If not, see
19 | // .
20 |
21 | program pasls;
22 |
23 | {$mode objfpc}{$H+}
24 |
25 | uses
26 | Classes, SysUtils, iostream, streamex, StreamIO,
27 | udebug, ubufferedreader, jsonstream,
28 | upackages, ujsonrpc, uinitialize, utextdocument, uutils,
29 | CastleLsp, ULogVSCode, UDocumentSymbolSupport, ushutdown, UWorkspaceSymbolSupport;
30 |
31 | var
32 | ShouldExit: Boolean;
33 |
34 | procedure SendError(
35 | Rpc: TRpcPeer; Id: TRpcId; Code: Integer; const Msg: string
36 | );
37 | var
38 | Response: TRpcResponse;
39 | begin
40 | Response := nil;
41 | try
42 | Response := TRpcResponse.CreateError(Id, Code, Msg);
43 | Rpc.Send(Response);
44 | finally
45 | FreeAndNil(Response);
46 | end;
47 | end;
48 |
49 | procedure Dispatch(Rpc: TRpcPeer; Request: TRpcRequest);
50 | begin
51 | { When there was shutdown request all other request should
52 | return InvalidRequest response, this is done by throwing ERpcError exception.
53 | more info: https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#shutdown}
54 | if WasShutdown and (Request.Method <> 'exit') then
55 | raise ERpcError.CreateFmt(
56 | jsrpcInvalidRequest,
57 | 'Request after shutdown: (method: %s)', [Request.Method]);
58 |
59 | if Request.Method = 'initialize' then
60 | Initialize(Rpc, Request)
61 | else if Request.Method = 'initialized' then
62 | else if Request.Method = 'shutdown' then
63 | Shutdown(Rpc, Request)
64 | else if Request.Method = 'textDocument/didOpen' then
65 | TextDocument_DidOpen(Rpc, Request)
66 | else if Request.Method = 'textDocument/didChange' then
67 | TextDocument_DidChange(Rpc, Request)
68 | else if Request.Method = 'textDocument/didClose' then
69 | else if Request.Method = 'textDocument/completion' then
70 | TextDocument_Completion(Rpc, Request)
71 | else if Request.Method = 'textDocument/signatureHelp' then
72 | TextDocument_SignatureHelp(Rpc, Request)
73 | else if Request.Method = 'textDocument/declaration' then
74 | TextDocument_Declaration(Rpc, Request)
75 | else if Request.Method = 'textDocument/definition' then
76 | TextDocument_Definition(Rpc, Request)
77 | else if Request.Method = 'textDocument/documentSymbol' then
78 | TextDocument_DocumentSymbol(Rpc, Request)
79 | else if Request.Method = 'workspace/symbol' then
80 | begin
81 | if EngineDeveloperMode then
82 | WorkspaceSymbol(Rpc, Request, WorkspaceAndEnginePaths)
83 | else
84 | WorkspaceSymbol(Rpc, Request, WorkspacePaths);
85 | end
86 | else if Request.Method = 'exit' then
87 | begin
88 | ShouldExit := true;
89 | DebugLog('Get exit message, exiting...');
90 | end
91 | else if Request.Method = '$/cancelRequest' then
92 | else if Request.Method = '$/setTrace' then
93 | TraceValue := ParseSetTrace(Request)
94 | else
95 | raise ERpcError.CreateFmt(
96 | jsrpcMethodNotFound, 'Method not found: %s', [Request.Method]
97 | );
98 | end;
99 |
100 | procedure Main(Rpc: TRpcPeer);
101 | var
102 | Request: TRpcRequest;
103 | begin
104 | while True do
105 | begin
106 | Request := nil;
107 | try
108 | Request := Rpc.Receive;
109 |
110 | if Request = nil then
111 | begin
112 | DebugLog('** End of stream, exiting **');
113 | exit;
114 | end;
115 |
116 | if ShouldExit then
117 | begin
118 | DebugLog('Main - exit');
119 | Exit;
120 | end;
121 |
122 | try
123 | Dispatch(Rpc, Request);
124 | except
125 | on E: ERpcError do
126 | SendError(Rpc, Request.Id, E.Code, E.Message);
127 |
128 | (*Catching all exceptions to prevent server from crashing,
129 | this seems the easiest solution to deal with various ways how
130 | Lazarus code tools can raise exception on invalid/in-progress code.
131 |
132 | E.g. TextDocument_DocumentSymbol failing for cge-effekseer:
133 | {"jsonrpc":"2.0","id":1,"method":"textDocument/documentSymbol","params":{"textDocument":{"uri":"file:///home/michalis/sources/castle-engine/cge-effekseer/src/CastleEffekseer.pas"}}}
134 | < Response:
135 | {"jsonrpc":"2.0","method":"window/logMessage","params":{"type":3,"message":"File name:/home/michalis/sources/castle-engine/cge-effekseer/src/CastleEffekseer.pas"}}
136 | FATAL EXCEPTION: expected :, but const found
137 |
138 | This change captures it and reports:
139 | Exception ECodeToolError while dispatching request textDocument/documentSymbol: expected :, but PassParams found
140 | *)
141 | on E: Exception do
142 | begin
143 | DebugLog('Exception %s while dispatching request %s: %s', [
144 | E.ClassName,
145 | Request.Method,
146 | E.Message
147 | ]);
148 | end;
149 | end;
150 | finally
151 | FreeAndNil(Request);
152 | end;
153 | end;
154 | end;
155 |
156 |
157 | var
158 | InputStream: TStream;
159 | OutputStream: TStream;
160 | DebugStream: TStream;
161 | Transcript: TStream;
162 | Tee: TStream;
163 |
164 | RpcPeer: TRpcPeer;
165 |
166 | TranscriptPath: string = '';
167 | LogPath: string = '';
168 | SaveReplay: Boolean = false;
169 | LoadReplay: Boolean = false;
170 |
171 | procedure PrintUsage;
172 | begin
173 | // TODO: Implement
174 | end;
175 |
176 | procedure ParseOptions;
177 | var
178 | i: integer;
179 | begin
180 | i := 1;
181 | while i <= ParamCount do
182 | begin
183 | if (ParamStr(i) = '--save-log') and (i < ParamCount) then
184 | begin
185 | LogPath := ParamStr(i + 1);
186 | Inc(i);
187 | end
188 | else if (ParamStr(i) = '--save-replay') and (i < ParamCount) then
189 | begin
190 | SaveReplay := true;
191 | TranscriptPath := ParamStr(i + 1);
192 | Inc(i);
193 | end
194 | else if (ParamStr(i) = '--replay') and (i < ParamCount) then
195 | begin
196 | LoadReplay := true;
197 | TranscriptPath := ParamStr(i + 1);
198 | Inc(i);
199 | end
200 | else
201 | begin
202 | PrintUsage;
203 | break;
204 | end;
205 | Inc(i);
206 | end;
207 | end;
208 |
209 | { Dump current exception backtrace. Copied from CastleClassUtils. }
210 | function DumpExceptionBackTraceToString: String;
211 | var
212 | TextFile: Text;
213 | StringStream: TStringStream;
214 | begin
215 | StringStream := TStringStream.Create('');
216 | try
217 | AssignStream(TextFile, StringStream);
218 | Rewrite(TextFile);
219 | try
220 | DumpExceptionBackTrace(TextFile);
221 | finally CloseFile(TextFile) end;
222 | Result := StringStream.DataString;
223 | finally FreeAndNil(StringStream) end;
224 | end;
225 |
226 | begin
227 | InputStream := nil;
228 | OutputStream := nil;
229 | DebugStream := nil;
230 | Transcript := nil;
231 | Tee := nil;
232 | RpcPeer := nil;
233 | ShouldExit := false;
234 | WasShutdown := false;
235 |
236 | ParseOptions;
237 |
238 | InitializeUserConfig;
239 | if LogPath = '' then
240 | begin
241 | LogPath := UserConfig.ReadString('log', 'filename', '');
242 | { If log specifies a filename, add suffix to make it unique per process,
243 | so that each pasls process doesn't try to open and write to the same file. }
244 | if LogPath <> '' then
245 | LogPath := LogPath + '.pid' + IntToStr(GetProcessID);
246 | end;
247 |
248 | if LogPath <> '' then
249 | try
250 | if LogPath = '-' then
251 | DebugStream := TIOStream.Create(iosError)
252 | else
253 | DebugStream := TFileStream.Create(LogPath, fmCreate);
254 | except
255 | DebugStream := TIOStream.Create(iosError);
256 | end;
257 |
258 | InitLog(DebugStream);
259 |
260 | if LoadReplay and SaveReplay then
261 | begin
262 | DebugLog('You specified both --save-replay and --replay. Ignoring.');
263 | LoadReplay := false;
264 | SaveReplay := false;
265 | end;
266 |
267 | try
268 | InputStream := TIOStream.Create(iosInput);
269 | OutputStream := TIOStream.Create(iosOutput);
270 |
271 | if SaveReplay then
272 | begin
273 | try
274 | Transcript := TFileStream.Create(TranscriptPath, fmCreate);
275 | except
276 | DebugLog(
277 | 'Could not create replay file "%s".',
278 | [TranscriptPath]
279 | );
280 | FreeAndNil(Transcript);
281 | end;
282 | Tee := TTeeStream.Create(InputStream, Transcript);
283 | RpcPeer := TRpcPeer.Create(Tee, OutputStream);
284 | end
285 | else if LoadReplay then
286 | begin
287 | InputStream := TFileStream.Create(TranscriptPath, fmOpenRead);
288 | RpcPeer := TRpcPeer.Create(InputStream, OutputStream);
289 | end
290 | else
291 | begin
292 | RpcPeer := TRpcPeer.Create(InputStream, OutputStream);
293 | end;
294 |
295 | try
296 | Main(RpcPeer);
297 | except
298 | on E: Exception do
299 | DebugLog('FATAL EXCEPTION: ' + E.Message +
300 | LineEnding + LineEnding +
301 | DumpExceptionBackTraceToString);
302 | end;
303 | finally
304 | FreeAndNil(InputStream);
305 | FreeAndNil(OutputStream);
306 | FreeAndNil(DebugStream);
307 | FreeAndNil(Transcript);
308 | FreeAndNil(Tee);
309 | FreeAndNil(RpcPeer);
310 | end;
311 |
312 | end.
313 |
--------------------------------------------------------------------------------
/server/ubufferedreader.pas:
--------------------------------------------------------------------------------
1 | // Pascal Language Server
2 | // Copyright 2021 Philip Zander
3 |
4 | // This file is part of Pascal Language Server.
5 |
6 | // Pascal Language Server is free software: you can redistribute it
7 | // and/or modify it under the terms of the GNU General Public License
8 | // as published by the Free Software Foundation, either version 3 of
9 | // the License, or (at your option) any later version.
10 |
11 | // Pascal Language Server is distributed in the hope that it will be
12 | // useful, but WITHOUT ANY WARRANTY; without even the implied warranty
13 | // of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 | // GNU General Public License for more details.
15 |
16 | // You should have received a copy of the GNU General Public License
17 | // along with Pascal Language Server. If not, see
18 | // .
19 |
20 | unit ubufferedreader;
21 |
22 | {$mode objfpc}{$H+}
23 |
24 | interface
25 |
26 | uses
27 | Classes, SysUtils;
28 |
29 | type
30 |
31 | { TBufferedReader }
32 |
33 | TBufferedReader = class
34 | protected
35 | FUnderlying: TStream;
36 | FBuf: array of Byte;
37 | FBufCapacity: LongInt;
38 | FBufSize: LongInt;
39 | FBufOffset: LongInt;
40 | FOwnsUnderlying: Boolean;
41 | public
42 | constructor Create(Underlying: TStream; OwnsUnderlying: Boolean=False);
43 | destructor Destroy; override;
44 |
45 | function Read(out Buf; n: LongInt): LongInt;
46 | function ReadLine: String;
47 | procedure BlockRead(out Buf; n: LongInt);
48 |
49 | property Underlying: TStream read FUnderlying;
50 | end;
51 |
52 | { TTeeStream }
53 |
54 | TTeeStream = class(TStream)
55 | protected
56 | FSource: TStream;
57 | FCC: TStream;
58 | public
59 | constructor Create(Source, CC: TStream);
60 | function Read(var Buffer; Count: Longint): Longint; override;
61 | end;
62 |
63 | implementation
64 |
65 | { TTeeStream }
66 |
67 | constructor TTeeStream.Create(Source, CC: TStream);
68 | begin
69 | inherited Create;
70 | FSource := Source;
71 | FCC := CC;
72 | end;
73 |
74 | function TTeeStream.Read(var Buffer; Count: Longint): Longint;
75 | begin
76 | Result := FSource.Read(Buffer, Count);
77 | if (Result > 0) and Assigned(FCC) then
78 | FCC.WriteBuffer(Buffer, Result);
79 | end;
80 |
81 | { TBufferedReader }
82 |
83 | constructor TBufferedReader.Create(
84 | Underlying: TStream; OwnsUnderlying: Boolean
85 | );
86 | begin
87 | FUnderlying := Underlying;
88 | FBufCapacity := 1024;
89 | FBufOffset := 0;
90 | FBufSize := 0;
91 | FOwnsUnderlying := OwnsUnderlying;
92 | SetLength(FBuf, FBufCapacity);
93 | end;
94 |
95 | destructor TBufferedReader.Destroy;
96 | begin
97 | if FOwnsUnderlying then
98 | FreeAndNil(FUnderlying);
99 | inherited Destroy;
100 | end;
101 |
102 | function TBufferedReader.Read(out Buf; n: LongInt): LongInt;
103 | var
104 | Available: LongInt;
105 | begin
106 | Available := FBufSize - FBufOffset;
107 | if Available > 0 then
108 | begin
109 | if Available < n then
110 | n := Available;
111 | Move(FBuf[FBufOffset], Buf, n);
112 | Inc(FBufOffset, n);
113 | Result := n;
114 | end
115 | else
116 | begin
117 | Result := Underlying.Read(Buf, n);
118 | end;
119 | end;
120 |
121 | function TBufferedReader.ReadLine: String;
122 | var
123 | idx, n, o: LongInt;
124 |
125 | function ScanAhead: LongInt;
126 | var
127 | i: integer;
128 | begin
129 | for i := FBufOffset to FBufSize - 1 do
130 | if (Chr(FBuf[i]) = #13) and
131 | ((i = FBufSize - 1) or (Chr(FBuf[i+1]) = #10)) then
132 | begin
133 | Result := i;
134 | exit;
135 | end;
136 | Result := FBufSize;
137 | end;
138 |
139 | begin
140 | Result := '';
141 | o := 0;
142 |
143 | while True do
144 | begin
145 | idx := ScanAhead;
146 | n := idx - FBufOffset;
147 |
148 | if n > 0 then
149 | begin
150 | SetLength(Result, o + n);
151 | Move(FBuf[FBufOffset], Result[o + 1], n);
152 | inc(o, n);
153 | end;
154 |
155 | FBufOffset := idx;
156 |
157 | // \r\n was found somewhere in the middle of the buffer
158 | if idx < FBufSize - 1 then
159 | begin
160 | inc(FBufOffset, 2);
161 | Exit;
162 | end
163 | // \r was found at the end of the buffer. Could be linebreak or not.
164 | else if idx = FBufSize - 1 then
165 | begin
166 | FBuf[0] := FBuf[idx];
167 | FBufOffset := 0;
168 | FBufSize := 1 + Underlying.Read(FBuf[1], FBufCapacity - 1);
169 | end
170 | // No (potential) linebreak was found in the buffer.
171 | else {if idx = FBufSize then}
172 | begin
173 | FBufOffset := 0;
174 | FBufSize := Underlying.Read(FBuf[0], FBufCapacity);
175 | end;
176 |
177 | if FBufSize = 0 then
178 | Exit;
179 | end;
180 |
181 | end;
182 |
183 | procedure TBufferedReader.BlockRead(out Buf; n: LongInt);
184 | var
185 | Total, Delta: LongInt;
186 | Ptr: PByte;
187 | begin
188 | assert(n > 0, 'Must read at least one byte');
189 | Ptr := PByte(@Buf);
190 | Total := 0;
191 | while Total < n do
192 | begin
193 | Delta := Read(Ptr^, n - Total);
194 | if Delta = 0 then
195 | raise EStreamError.Create('Stream abruptly ended');
196 | Inc(Total, Delta);
197 | Inc(Ptr, Delta);
198 | end;
199 | end;
200 |
201 | end.
202 |
203 |
--------------------------------------------------------------------------------
/server/udebug.pas:
--------------------------------------------------------------------------------
1 | // Pascal Language Server
2 | // Copyright 2021 Philip Zander
3 |
4 | // This file is part of Pascal Language Server.
5 |
6 | // Pascal Language Server is free software: you can redistribute it
7 | // and/or modify it under the terms of the GNU General Public License
8 | // as published by the Free Software Foundation, either version 3 of
9 | // the License, or (at your option) any later version.
10 |
11 | // Pascal Language Server is distributed in the hope that it will be
12 | // useful, but WITHOUT ANY WARRANTY; without even the implied warranty
13 | // of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 | // GNU General Public License for more details.
15 |
16 | // You should have received a copy of the GNU General Public License
17 | // along with Pascal Language Server. If not, see
18 | // .
19 |
20 | unit udebug;
21 |
22 | {$mode objfpc}{$H+}
23 |
24 | interface
25 |
26 | uses
27 | Classes, SysUtils;
28 |
29 | procedure InitLog(Destination: TStream);
30 | procedure DebugLog(const Msg: string); overload;
31 | procedure DebugLog(const Fmt: string; Args: array of const); overload;
32 |
33 | implementation
34 |
35 | var
36 | DebugOutput: TStream;
37 |
38 | procedure InitLog(Destination: TStream);
39 | begin
40 | DebugOutput := Destination;
41 | end;
42 |
43 | procedure DebugLog(const Msg: string);
44 | begin
45 | if (DebugOutput <> nil) and (Msg <> '') then
46 | DebugOutput.WriteBuffer(Msg[1], Length(Msg));
47 | end;
48 |
49 | procedure DebugLog(const Fmt: string; Args: array of const);
50 | var
51 | s: string;
52 | begin
53 | s := Format(Fmt, Args) + LineEnding;
54 | DebugLog(s);
55 | end;
56 |
57 | end.
58 |
59 |
--------------------------------------------------------------------------------
/server/udocumentsymbolsupport.pas:
--------------------------------------------------------------------------------
1 | unit UDocumentSymbolSupport;
2 |
3 | {
4 | Implementation of DocumentSymbol
5 |
6 | Docs: https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#documentSymbol
7 | }
8 |
9 | {$mode ObjFPC}{$H+}
10 |
11 | interface
12 |
13 | uses
14 | Classes, SysUtils, jsonstream, ujsonrpc, uutils;
15 |
16 | type
17 | { Enumeration of symbol kinds based on
18 | https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#symbolKind }
19 | TDocumentSymbolKind = (
20 | dskFile = 1,
21 | dskModule,
22 | dskNamespace,
23 | dskPackage,
24 | dskClass,
25 | dskMethod,
26 | dskProperty,
27 | dskField,
28 | dskConstructor,
29 | dskEnum,
30 | dskInterface,
31 | dskFunction,
32 | dskVariable,
33 | dskConstant,
34 | dskString,
35 | dskNumber,
36 | dskBoolean,
37 | dskArray,
38 | dskObject,
39 | dskKey,
40 | dskNull,
41 | dskEnumMember,
42 | dskStruct,
43 | dskEvent,
44 | dskOperator,
45 | dskTypeOperator
46 | );
47 |
48 | TSymbolTag = (
49 | stDeprecated
50 | );
51 |
52 | TSymbolTags = set of TSymbolTag;
53 |
54 | procedure TextDocument_DocumentSymbol(const Rpc: TRpcPeer; const Request: TRpcRequest);
55 |
56 | implementation
57 |
58 | uses ulogvscode, CodeToolManager, CodeCache, CodeTree, PascalParserTool;
59 |
60 | function ParseDocumentSymbolRequest(Reader: TJsonReader): String;
61 | var
62 | Key, Uri: String;
63 | begin
64 | Uri := '';
65 | if Reader.Dict then
66 | while (Reader.Advance <> jsDictEnd) and Reader.Key(Key) do
67 | begin
68 | if (Key = 'textDocument') and Reader.Dict then
69 | while (Reader.Advance <> jsDictEnd) and Reader.Key(Key) do
70 | begin
71 | if Key = 'uri' then
72 | begin
73 | Reader.Str(Uri);
74 | break;
75 | end;
76 | end
77 | end;
78 | Result := URIToFileNameEasy(Uri);
79 | end;
80 |
81 | { Return null response, that signals "no error, but also no result". }
82 | procedure SendNullResponse(const Rpc: TRpcPeer; const Request: TRpcRequest);
83 | var
84 | Response: TRpcResponse;
85 | Writer: TJsonWriter;
86 | begin
87 | Response := TRpcResponse.Create(Request.Id);
88 | try
89 | Writer := Response.Writer;
90 | Writer.Null;
91 | Rpc.Send(Response);
92 | finally
93 | FreeAndNil(Response);
94 | end;
95 | end;
96 |
97 | { Responses for textDocument/documentSymbol method
98 | Docs: https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_documentSymbol }
99 | procedure TextDocument_DocumentSymbol(const Rpc: TRpcPeer; const Request: TRpcRequest);
100 | var
101 | Filename: String;
102 | Code: TCodeBuffer;
103 | CodeTool: TCodeTool;
104 | CodeTreeNode: TCodeTreeNode;
105 |
106 | Node: TCodeTreeNode;
107 |
108 | Response: TRpcResponse;
109 | Writer: TJsonWriter;
110 |
111 | StartCaret: TCodeXYPosition;
112 | EndCaret: TCodeXYPosition;
113 | ProcedureName: String;
114 | begin
115 | Filename := ParseDocumentSymbolRequest(Request.Reader);
116 | LogInfo(Rpc, 'File name:' + Filename);
117 | Code := CodeToolBoss.FindFile(Filename);
118 |
119 | if Code = nil then
120 | raise ERpcError.CreateFmt(
121 | jsrpcInvalidRequest,
122 | 'File not found: %s', [Filename]
123 | );
124 |
125 | { Based on lazarus TProcedureListForm.GetCodeTreeNode() }
126 |
127 | CodeToolBoss.Explore(Code, CodeTool, false, false);
128 |
129 | if CodeTool = nil then
130 | begin
131 | { This happens when opening include file without MainUnit,
132 | like https://github.com/castle-engine/castle-engine/blob/master/src/common_includes/castleconf.inc .
133 | Return null (not any error) in response.}
134 | SendNullResponse(Rpc, Request);
135 | Exit;
136 | end;
137 |
138 | if CodeTool.Tree = nil then
139 | raise ERpcError.Create(jsrpcRequestFailed, 'Code tool tree is nil.');
140 |
141 | if CodeTool.Tree.Root = nil then
142 | begin
143 | { This happens when pas file is empty, return null (not any error) in response. }
144 | SendNullResponse(Rpc, Request);
145 | Exit;
146 | end;
147 |
148 | { Search for implementation node }
149 | try
150 | CodeTreeNode := CodeTool.FindImplementationNode;
151 | except
152 | on E: Exception do
153 | raise ERpcError.Create(jsrpcRequestFailed, 'FindImplementationNode exception: ' + E.Message);
154 | end;
155 |
156 | { When there is no implementation section try to parse interface }
157 | if CodeTreeNode = nil then
158 | CodeTreeNode := CodeTool.FindInterfaceNode;
159 |
160 | if CodeTreeNode = nil then
161 | begin
162 | { This happens when there is no interface and implementation in file,
163 | return null (not any error) in response. }
164 | SendNullResponse(Rpc, Request);
165 | Exit;
166 | end;
167 |
168 | Response := nil;
169 | try
170 | Response := TRpcResponse.Create(Request.Id);
171 | Writer := Response.Writer;
172 |
173 | Writer.List;
174 | { Based on lazarus TProcedureListForm.AddToGrid() and other functions }
175 | Node := CodeTreeNode;
176 | while Node <> nil do
177 | begin
178 | // LogInfo(Rpc, 'Node: ' + Node.DescAsString);
179 | if Node.Desc = ctnProcedure then
180 | begin
181 | { LogInfo(Rpc, CodeTool.ExtractProcHead(Node, [phpAddParentProcs,
182 | phpWithoutParamList, phpWithoutBrackets, phpWithoutSemicolon])); }
183 |
184 | { Get the real position in source file }
185 | CodeTool.CleanPosToCaret(Node.StartPos, StartCaret);
186 | CodeTool.CleanPosToCaret(Node.EndPos, EndCaret);
187 |
188 | { Inc file support: do not add procedures those demand jump to another
189 | include file that makes they do not work }
190 |
191 | //LogInfo(Rpc, 'Caret file name ' + StartCaret.Code.Filename);
192 | //LogInfo(Rpc, 'Filename ' + Filename);
193 | if not SameFileName(StartCaret.Code.Filename, Filename) then
194 | begin
195 | Node := Node.Next;
196 | continue;
197 | end;
198 |
199 | ProcedureName := CodeTool.ExtractProcHead(Node, [phpAddParentProcs,
200 | phpWithoutParamList, phpWithoutBrackets, phpWithoutSemicolon]);
201 |
202 | { Check procedure name is not empty, that makes vscode returns errors.
203 | Can happen when we start write new procedure. }
204 | if Trim(ProcedureName) = '' then
205 | begin
206 | Node := Node.Next;
207 | continue;
208 | end;
209 |
210 | Writer.Dict;
211 | Writer.Key('name');
212 | Writer.Str(ProcedureName);
213 |
214 | Writer.Key('kind');
215 | Writer.Number(Integer(dskMethod));
216 |
217 |
218 | Writer.Key('range');
219 | Writer.Dict;
220 | Writer.Key('start');
221 | Writer.Dict;
222 | Writer.Key('line');
223 | Writer.Number(StartCaret.Y);
224 | Writer.Key('character');
225 | Writer.Number(StartCaret.X);
226 | Writer.DictEnd;
227 | Writer.Key('end');
228 | Writer.Dict;
229 | Writer.Key('line');
230 | Writer.Number(EndCaret.Y);
231 | Writer.Key('character');
232 | Writer.Number(EndCaret.X);
233 | Writer.DictEnd;
234 | Writer.DictEnd;
235 |
236 | Writer.Key('selectionRange');
237 | Writer.Dict;
238 | Writer.Key('start');
239 | Writer.Dict;
240 | Writer.Key('line');
241 | Writer.Number(StartCaret.Y);
242 | Writer.Key('character');
243 | Writer.Number(StartCaret.X);
244 | Writer.DictEnd;
245 | Writer.Key('end');
246 | Writer.Dict;
247 | Writer.Key('line');
248 | Writer.Number(StartCaret.Y);
249 | Writer.Key('character');
250 | Writer.Number(StartCaret.X);
251 | Writer.DictEnd;
252 | Writer.DictEnd;
253 |
254 | Writer.DictEnd;
255 | end;
256 | Node := Node.Next;
257 | end;
258 | Writer.ListEnd;
259 | Rpc.Send(Response);
260 | finally
261 | FreeAndNil(Response);
262 | end;
263 | end;
264 |
265 | end.
266 |
267 |
--------------------------------------------------------------------------------
/server/uinitialize.pas:
--------------------------------------------------------------------------------
1 | // Pascal Language Server
2 | // Copyright 2020 Arjan Adriaanse
3 | // 2021 Philip Zander
4 |
5 | // This file is part of Pascal Language Server.
6 |
7 | // Pascal Language Server is free software: you can redistribute it
8 | // and/or modify it under the terms of the GNU General Public License
9 | // as published by the Free Software Foundation, either version 3 of
10 | // the License, or (at your option) any later version.
11 |
12 | // Pascal Language Server is distributed in the hope that it will be
13 | // useful, but WITHOUT ANY WARRANTY; without even the implied warranty
14 | // of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 | // GNU General Public License for more details.
16 |
17 | // You should have received a copy of the GNU General Public License
18 | // along with Pascal Language Server. If not, see
19 | // .
20 |
21 | unit uinitialize;
22 |
23 | {$mode objfpc}{$H+}
24 |
25 | interface
26 |
27 | uses
28 | jsonstream, ujsonrpc;
29 |
30 | procedure Initialize(Rpc: TRpcPeer; Request: TRpcRequest);
31 |
32 | implementation
33 |
34 | uses
35 | SysUtils, Classes, CodeToolManager, CodeToolsConfig, URIParser, LazUTF8,
36 | DefineTemplates, FileUtil, LazFileUtils, LCLVersion, IdentCompletionTool,
37 | DOM, XMLRead, udebug, uutils, upackages, utextdocument,
38 | CastleLsp, CastleArchitectures, ULogVSCode;
39 |
40 | // Resolve the dependencies of Pkg, and then the dependencies of the
41 | // dependencies and so on. Uses global registry and paths locally specified in
42 | // the package/project file (.lpk/.lpi) as a data source.
43 | procedure ResolveDeps(Pkg: TPackage);
44 | var
45 | Dep: ^TDependency;
46 | DepPath: string;
47 | i: integer;
48 | function IfThen(Cond: Boolean; const s: string): string;
49 | begin
50 | if Cond then
51 | Result := s
52 | else
53 | Result := '';
54 | end;
55 | begin
56 | if Pkg.DidResolveDeps then
57 | exit;
58 |
59 | Pkg.DidResolveDeps := True;
60 |
61 | for i := low(Pkg.Dependencies) to high(Pkg.Dependencies) do
62 | begin
63 | Dep := @Pkg.Dependencies[i];
64 |
65 | DepPath := LookupGlobalPackage(Dep^.Name);
66 | if (Dep^.Prefer) or (DepPath = '') then
67 | DepPath := Dep^.Path;
68 |
69 | if DepPath = '' then
70 | begin
71 | DebugLog(' Dependency %s: not found', [Dep^.Name]);
72 | continue;
73 | end;
74 |
75 | DebugLog(
76 | ' Dependency: %s -> %s%s',
77 | [Dep^.Name, DepPath, IfThen(DepPath = Dep^.Path, ' (hardcoded)')]
78 | );
79 |
80 | Dep^.Package := GetPackageOrProject(DepPath);
81 |
82 | // Add ourselves to the RequiredBy list of the dependency.
83 | SetLength(Dep^.Package.RequiredBy, Length(Dep^.Package.RequiredBy) + 1);
84 | Dep^.Package.RequiredBy[High(Dep^.Package.RequiredBy)] := Pkg;
85 |
86 | // Recurse
87 | ResolveDeps(Dep^.Package);
88 | end;
89 | end;
90 |
91 | // Try to fix missing dependencies.
92 | //
93 | // Consider the following scenario:
94 | //
95 | // A requires:
96 | // - B (found)
97 | // - C (NOT found)
98 | // B requires:
99 | // - C (found)
100 | //
101 | // In other words, we could not find C for A, but did find C for B. (The
102 | // reason for this might be that B specified a default or preferred path for
103 | // dependency C). In this case we resolve the situation by using B's C also
104 | // for A.
105 | procedure GuessMissingDependencies(Pkg: TPackage);
106 | var
107 | Dep: ^TDependency;
108 | i: Integer;
109 |
110 | // Breadth-first search for a package of the specified name in the
111 | // dependencies of Node.
112 | function GuessDependency(Node: TPackage; DepName: String): TPackage;
113 | var
114 | j: integer;
115 | begin
116 | Result := nil;
117 |
118 | if Node.Visited then
119 | exit;
120 |
121 | Node.Visited := True;
122 | try
123 | for j := low(Node.Dependencies) to high(Node.Dependencies) do
124 | begin
125 | if (UpperCase(DepName) = UpperCase(Node.Dependencies[j].Name)) and
126 | Assigned(Node.Dependencies[j].Package) then
127 | begin
128 | Result := Node.Dependencies[j].Package;
129 | exit;
130 | end;
131 | end;
132 |
133 | // Not found, recurse
134 | for j := low(Node.RequiredBy) to high(Node.RequiredBy) do
135 | begin
136 | Result := GuessDependency(Node.RequiredBy[j], DepName);
137 | if Assigned(Result) then
138 | exit;
139 | end;
140 |
141 | finally
142 | Node.Visited := False;
143 | end;
144 | end;
145 | begin
146 | for i := low(Pkg.Dependencies) to high(Pkg.Dependencies) do
147 | begin
148 | Dep := @Pkg.Dependencies[i];
149 | if Assigned(Dep^.Package) then
150 | continue;
151 |
152 | Dep^.Package := GuessDependency(Pkg, Dep^.Name);
153 | end;
154 | end;
155 |
156 | // Add the search paths of its dependencies to a package.
157 | procedure ResolvePaths(Pkg: TPackage);
158 | var
159 | Dep: TDependency;
160 | begin
161 | if Pkg.DidResolvePaths then
162 | exit;
163 |
164 | Pkg.DidResolvePaths := True;
165 |
166 | Pkg.ResolvedPaths := Pkg.Paths;
167 |
168 | for Dep in Pkg.Dependencies do
169 | begin
170 | if not Assigned(Dep.Package) then
171 | continue;
172 |
173 | // Recurse
174 | ResolvePaths(Dep.Package);
175 |
176 | Pkg.ResolvedPaths.IncludePath := MergePaths([
177 | Pkg.ResolvedPaths.IncludePath{,
178 | Dep.Package.ResolvedPaths.IncludePath}
179 | ]);
180 | Pkg.ResolvedPaths.UnitPath := MergePaths([
181 | Pkg.ResolvedPaths.UnitPath,
182 | Dep.Package.ResolvedPaths.UnitPath
183 | ]);
184 | Pkg.ResolvedPaths.SrcPath := MergePaths([
185 | Pkg.ResolvedPaths.SrcPath{,
186 | Dep.Package.ResolvedPaths.SrcPath}
187 | ]);
188 | end;
189 | end;
190 |
191 | // Add required search paths to package's source directories (and their
192 | // subdirectories).
193 | procedure ConfigurePackage(Pkg: TPackage);
194 | var
195 | Dep: TDependency;
196 | OtherSrc: TStringArray;
197 | OtherDir: string;
198 |
199 | procedure ConfigureSearchPath(const Dir: string);
200 | var
201 | DirectoryTemplate,
202 | IncludeTemplate,
203 | UnitPathTemplate,
204 | SrcTemplate: TDefineTemplate;
205 | Paths: TPaths;
206 | begin
207 | DirectoryTemplate := TDefineTemplate.Create(
208 | 'Directory', '',
209 | '', Dir,
210 | da_Directory
211 | );
212 |
213 | Paths.UnitPath := MergePaths([UnitPathMacro, Pkg.ResolvedPaths.UnitPath]);
214 | Paths.IncludePath := MergePaths([IncludePathMacro, Pkg.ResolvedPaths.IncludePath]);
215 | Paths.SrcPath := MergePaths([SrcPathMacro, Pkg.ResolvedPaths.SrcPath]);
216 |
217 | DebugLog('%s', [Dir]);
218 | DebugLog(' UnitPath: %s', [Paths.UnitPath]);
219 | DebugLog(' IncludePath: %s', [Paths.IncludePath]);
220 | DebugLog(' SrcPath: %s', [Paths.SrcPath]);
221 |
222 | UnitPathTemplate := TDefineTemplate.Create(
223 | 'Add to the UnitPath', '',
224 | UnitPathMacroName, Paths.UnitPath,
225 | da_DefineRecurse
226 | );
227 |
228 | IncludeTemplate := TDefineTemplate.Create(
229 | 'Add to the Include path', '',
230 | IncludePathMacroName, Paths.IncludePath,
231 | da_DefineRecurse
232 | );
233 |
234 | SrcTemplate := TDefineTemplate.Create(
235 | 'Add to the Src path', '',
236 | SrcPathMacroName, Paths.SrcPath,
237 | da_DefineRecurse
238 | );
239 |
240 | DirectoryTemplate.AddChild(UnitPathTemplate);
241 | DirectoryTemplate.AddChild(IncludeTemplate);
242 | DirectoryTemplate.AddChild(SrcTemplate);
243 |
244 | CodeToolBoss.DefineTree.Add(DirectoryTemplate);
245 | end;
246 | begin
247 | if Pkg.Configured then
248 | exit;
249 | Pkg.Configured := True;
250 |
251 | // Configure search path for package's (or project's) main source directory.
252 | ConfigureSearchPath(Pkg.Dir);
253 |
254 | // Configure search path for other listed source directories.
255 | OtherSrc := Pkg.Paths.SrcPath.Split([';'], TStringSplitOptions.ExcludeEmpty);
256 | for OtherDir in OtherSrc do
257 | ConfigureSearchPath(OtherDir);
258 |
259 | // Recurse
260 | for Dep in Pkg.Dependencies do
261 | begin
262 | if not Assigned(Dep.Package) then
263 | continue;
264 | ConfigurePackage(Dep.Package);
265 | end;
266 | end;
267 |
268 | // Don't load packages from directories with these names...
269 | function IgnoreDirectory(const Dir: string): Boolean;
270 | var
271 | DirName: string;
272 | begin
273 | Dirname := lowercase(ExtractFileName(Dir));
274 | Result :=
275 | (DirName = '.git') or
276 | ((Length(DirName) >= 1) and (DirName[1] = '.')) or
277 | (DirName = 'backup') or
278 | (DirName = 'lib') or
279 | (Pos('.dsym', DirName) > 0) or
280 | (Pos('.app', DirName) > 0);
281 | end;
282 |
283 | // Load all packages in a directory and its subdirectories.
284 | procedure LoadAllPackagesUnderPath(const Dir: string);
285 | var
286 | Packages,
287 | SubDirectories: TStringList;
288 | i: integer;
289 | Pkg: TPackage;
290 | begin
291 | if IgnoreDirectory(Dir) then
292 | Exit;
293 |
294 | try
295 | Packages := FindAllFiles(
296 | Dir, '*.lpi;*.lpk', False, faAnyFile and not faDirectory
297 | );
298 |
299 | for i := 0 to Packages.Count - 1 do
300 | begin
301 | Pkg := GetPackageOrProject(Packages[i]);
302 | ResolveDeps(Pkg);
303 | end;
304 |
305 | // Recurse into child directories
306 |
307 | SubDirectories := FindAllDirectories(Dir, False);
308 | for i := 0 to SubDirectories.Count - 1 do
309 | LoadAllPackagesUnderPath(SubDirectories[i]);
310 |
311 | finally
312 | if Assigned(Packages) then
313 | FreeAndNil(Packages);
314 | if Assigned(Packages) then
315 | FreeAndNil(SubDirectories);
316 | end;
317 | end;
318 |
319 | // Given a directory, fix missing deps for all packages in the directory.
320 | procedure GuessMissingDepsForAllPackages(const Dir: string);
321 | var
322 | Packages,
323 | SubDirectories: TStringList;
324 | i: integer;
325 | Pkg: TPackage;
326 | begin
327 | if IgnoreDirectory(Dir) then
328 | Exit;
329 |
330 | try
331 | Packages := FindAllFiles(
332 | Dir, '*.lpi;*.lpk', False, faAnyFile and not faDirectory
333 | );
334 |
335 | for i := 0 to Packages.Count - 1 do
336 | begin
337 | Pkg := GetPackageOrProject(Packages[i]);
338 | GuessMissingDependencies(Pkg);
339 | end;
340 |
341 | // Recurse into child directories
342 | SubDirectories := FindAllDirectories(Dir, False);
343 | for i := 0 to SubDirectories.Count - 1 do
344 | GuessMissingDepsForAllPackages(SubDirectories[i]);
345 |
346 | finally
347 | if Assigned(Packages) then
348 | FreeAndNil(Packages);
349 | if Assigned(Packages) then
350 | FreeAndNil(SubDirectories);
351 | end;
352 | end;
353 |
354 | // Use heuristic to add search paths to the directory 'Dir'.
355 | // If there are any projects (.lpi) or packages (.lpk) in the directory, use
356 | // (only) their search paths. Otherwise, inherit the search paths from the
357 | // parent directory ('ParentPaths').
358 | procedure ConfigurePaths(const Dir: string);
359 | var
360 | Packages,
361 | SubDirectories: TStringList;
362 | i: integer;
363 |
364 | DirectoryTemplate,
365 | IncludeTemplate,
366 | UnitPathTemplate: TDefineTemplate;
367 | Pkg: TPackage;
368 |
369 | begin
370 | if IgnoreDirectory(Dir) then
371 | Exit;
372 |
373 | Packages := nil;
374 | SubDirectories := nil;
375 | try
376 | // 1. Add local files to search path of current directory
377 | DirectoryTemplate := TDefineTemplate.Create(
378 | 'Directory', '',
379 | '', Dir,
380 | da_Directory
381 | );
382 | UnitPathTemplate := TDefineTemplate.Create(
383 | 'Add to the UnitPath', '',
384 | UnitPathMacroName, MergePaths([UnitPathMacro, Dir]),
385 | da_Define
386 | );
387 | IncludeTemplate := TDefineTemplate.Create(
388 | 'Add to the Include path', '',
389 | IncludePathMacroName, MergePaths([IncludePathMacro, Dir]),
390 | da_Define
391 | );
392 | DirectoryTemplate.AddChild(UnitPathTemplate);
393 | DirectoryTemplate.AddChild(IncludeTemplate);
394 | CodeToolBoss.DefineTree.Add(DirectoryTemplate);
395 |
396 | // 2. Load all packages in the current directory and configure their
397 | // paths.
398 | Packages := FindAllFiles(
399 | Dir, '*.lpi;*.lpk', False, faAnyFile and not faDirectory
400 | );
401 |
402 | // 2a. Recursively resolve search paths for each package.
403 | // (Merge dependencies' search paths into own search path)
404 | for i := 0 to Packages.Count - 1 do
405 | begin
406 | Pkg := GetPackageOrProject(Packages[i]);
407 | ResolvePaths(Pkg);
408 | end;
409 |
410 | // 2b. For each package in the dependency tree, apply the package's
411 | // resulting search paths from the previous step to the package's source
412 | // directories. ("apply" = add to the CodeTools Define Tree)
413 | for i := 0 to Packages.Count - 1 do
414 | begin
415 | Pkg := GetPackageOrProject(Packages[i]);
416 | ConfigurePackage(Pkg);
417 | end;
418 |
419 | // Recurse into child directories
420 | SubDirectories := FindAllDirectories(Dir, False);
421 | for i := 0 to SubDirectories.Count - 1 do
422 | ConfigurePaths(SubDirectories[i]);
423 | finally
424 | if Assigned(Packages) then
425 | FreeAndNil(Packages);
426 | if Assigned(Packages) then
427 | FreeAndNil(SubDirectories);
428 | end;
429 | end;
430 |
431 | // CodeTools needs to know the paths for the global packages, the FPC source
432 | // files, the path of the compiler and the target architecture.
433 | // Attempt to guess the correct settings from Lazarus config files.
434 | procedure GuessCodeToolConfig(Options: TCodeToolsOptions);
435 | var
436 | ConfigDirs: TStringList;
437 | Dir: string;
438 | Doc: TXMLDocument;
439 |
440 | Root,
441 | EnvironmentOptions,
442 | FPCConfigs,
443 | Item1: TDomNode;
444 |
445 | LazarusDirectory,
446 | FPCSourceDirectory,
447 | CompilerFilename,
448 | OS, CPU: string;
449 |
450 | function LoadLazConfig(Path: string): Boolean;
451 | begin
452 | Doc := nil;
453 | Root := nil;
454 | Result := false;
455 | try
456 | ReadXMLFile(Doc, Path);
457 | Root := Doc.DocumentElement;
458 | if Root.NodeName = 'CONFIG' then
459 | Result := true;
460 | DebugLog('Reading config from %s', [Path]);
461 | except
462 | // Swallow
463 | end;
464 | end;
465 |
466 | function GetVal(Parent: TDomNode; Ident: string; Attr: string='Value'): string;
467 | var
468 | Node, Value: TDomNode;
469 | begin
470 | Result := '';
471 | if Parent = nil then
472 | exit;
473 | Node := Parent.FindNode(DOMString(Ident));
474 | if Node = nil then
475 | exit;
476 | Value := Node.Attributes.GetNamedItem(DOMString(Attr));
477 | if Value = nil then
478 | exit;
479 | Result := string(Value.NodeValue);
480 | end;
481 |
482 | var
483 | CustomLazarusConfigDir: String;
484 | begin
485 | { Preparing potential configuration folders and then trying to read
486 | the settings from all of them one by one.
487 | TODO: There can be potentional problem with order of checked path. }
488 |
489 | ConfigDirs := TStringList.Create;
490 | try
491 | CustomLazarusConfigDir := UserConfig.ReadString('lazarus', 'config', '');
492 | if CustomLazarusConfigDir <> '' then
493 | ConfigDirs.Add(ExcludeTrailingPathDelimiter(CustomLazarusConfigDir));
494 |
495 | // add folder like C:\Users\\AppData\Local\lazarus\
496 | ConfigDirs.Add(GetConfigDirForApp('lazarus', '', False));
497 |
498 | // add folder like C:\Users\\\.lazarus
499 | ConfigDirs.Add(GetUserDir + DirectorySeparator + '.lazarus');
500 |
501 | // add folder like C:\ProgramData\lazarus\
502 | ConfigDirs.Add(GetConfigDirForApp('lazarus', '', True)); ;
503 |
504 | for Dir in ConfigDirs do
505 | begin
506 | Doc := nil;
507 | try
508 | if LoadLazConfig(Dir + DirectorySeparator + 'environmentoptions.xml') then
509 | begin
510 | EnvironmentOptions := Root.FindNode('EnvironmentOptions');
511 | LazarusDirectory := GetVal(EnvironmentOptions, 'LazarusDirectory');
512 | FPCSourceDirectory := GetVal(EnvironmentOptions, 'FPCSourceDirectory');
513 | CompilerFilename := GetVal(EnvironmentOptions, 'CompilerFilename');
514 | if (Options.LazarusSrcDir = '') and (LazarusDirectory <> '') then
515 | Options.LazarusSrcDir := LazarusDirectory;
516 | if (Options.FPCSrcDir = '') and (FPCSourceDirectory <> '') then
517 | Options.FPCSrcDir := FPCSourceDirectory;
518 | if (Options.FPCPath = '') and (CompilerFilename <> '') then
519 | Options.FPCPath := CompilerFilename;
520 | end;
521 | finally
522 | FreeAndNil(Doc);
523 | end;
524 |
525 | Doc := nil;
526 | try
527 | if LoadLazConfig(Dir + DirectorySeparator + 'fpcdefines.xml') then
528 | begin
529 | FPCConfigs := Root.FindNode('FPCConfigs');
530 | Item1 := nil;
531 | if Assigned(FPCConfigs) and (FPCConfigs.ChildNodes.Count > 0) then
532 | Item1 := FPCConfigs.ChildNodes[0];
533 | OS := GetVal(Item1, 'RealCompiler', 'OS');
534 | CPU := GetVal(Item1, 'RealCompiler', 'CPU');
535 | if (Options.TargetOS = '') and (OS <> '') then
536 | Options.TargetOS := OS;
537 | if (Options.TargetProcessor = '') and (CPU <> '') then
538 | Options.TargetProcessor := CPU;
539 | end;
540 | finally
541 | FreeAndNil(Doc);
542 | end;
543 | end;
544 | finally
545 | FreeAndNil(ConfigDirs);
546 | end;
547 | end;
548 |
549 | procedure Initialize(Rpc: TRpcPeer; Request: TRpcRequest);
550 |
551 | function SyntaxErrorReportingModeFromInt(const I: Integer): TSyntaxErrorReportingMode;
552 | begin
553 | if (I < Ord(Low(TSyntaxErrorReportingMode))) or
554 | (I > Ord(High(TSyntaxErrorReportingMode))) then
555 | raise Exception.CreateFmt('Invalid syntaxErrorReportingMode: %d, ignoring', [I]);
556 |
557 | Result := TSyntaxErrorReportingMode(I)
558 | end;
559 |
560 | function ParseStandardUnitsPaths(const StandardUnitsPaths: String): String;
561 | var
562 | I: Integer;
563 | StandardUnitsPathsList: TStringList;
564 | begin
565 | DebugLog('StandardUnitsPaths from VSCode: ' + StandardUnitsPaths + LineEnding);
566 |
567 | Result := '';
568 | if Trim(StandardUnitsPaths) = '' then
569 | Exit;
570 |
571 | StandardUnitsPathsList := TStringList.Create;
572 | try
573 | StandardUnitsPathsList.Text := StandardUnitsPaths;
574 | for i := 0 to StandardUnitsPathsList.Count -1 do
575 | begin
576 | if Trim(StandardUnitsPathsList[i]) = '' then
577 | continue;
578 | Result := Result + ' -Fu"' + StandardUnitsPathsList[i] + '"';
579 | end;
580 | finally
581 | FreeAndNil(StandardUnitsPathsList);
582 | end;
583 | end;
584 |
585 | function ParseProjectSearchPaths(
586 | const ProjectSearchPaths, ProjectDirectory: String): String;
587 | var
588 | I: Integer;
589 | ProjectSearchPathsList: TStringList;
590 | begin
591 | DebugLog('ProjectSearchPaths from VSCode: ' + ProjectSearchPaths + LineEnding);
592 |
593 | Result := '';
594 | if Trim(ProjectSearchPaths) = '' then
595 | Exit;
596 |
597 | ProjectSearchPathsList := TStringList.Create;
598 | try
599 | ProjectSearchPathsList.Text := ProjectSearchPaths;
600 | for i := 0 to ProjectSearchPathsList.Count -1 do
601 | begin
602 | if Trim(ProjectSearchPathsList[i]) = '' then
603 | continue;
604 | Result := Result + ' -Fu"' +
605 | IncludeTrailingPathDelimiter(ProjectDirectory) +
606 | ProjectSearchPathsList[i] + '"';
607 | end;
608 | finally
609 | FreeAndNil(ProjectSearchPathsList);
610 | end;
611 | end;
612 |
613 | var
614 | Options: TCodeToolsOptions;
615 | Key: string;
616 | s: string;
617 | i: Integer;
618 | b: Boolean;
619 | ExtraOptions: String;
620 |
621 | RootUri: string;
622 | Directory: string;
623 | StandardUnitsPaths: string;
624 | ProjectSearchPaths: string;
625 | Response: TRpcResponse;
626 | Reader: TJsonReader;
627 | Writer: TJsonWriter;
628 | begin
629 | Options := nil;
630 | Response := nil;
631 | EngineDeveloperMode := false;
632 |
633 | try
634 | Options := TCodeToolsOptions.Create;
635 | Options.InitWithEnvironmentVariables;
636 |
637 | {
638 | // We no longer hardcode the paths, instead we try to load them from the
639 | // Lazarus config files or from the initializationOptions passed by the
640 | // Initialize RPC call (or environment variables).
641 | // I'm just leaving this here as a reminder of what these variables should
642 | // approximately look like.
643 | with Options do
644 | begin
645 | ProjectDir := Directory;
646 | TargetOS := 'Darwin';
647 | TargetProcessor := 'x86_64';
648 | FPCSrcDir := '/usr/local/share/fpcsrc/3.2.0';
649 | LazarusSrcDir := '/Applications/Lazarus';
650 | FPCPath := '/usr/local/bin/fpc';
651 | TestPascalFile := '/tmp/testfile1.pas';
652 | end;
653 | }
654 |
655 | // Parse initializationOptions
656 | Reader := Request.Reader;
657 | if Reader.Dict then
658 | while (Reader.Advance <> jsDictEnd) and Reader.Key(Key) do
659 | begin
660 | if Key = 'rootUri' then
661 | Reader.Str(RootUri)
662 | else if (Key = 'initializationOptions') and Reader.Dict then
663 | while (Reader.Advance <> jsDictEnd) and Reader.Key(Key) do
664 | begin
665 | if (Key = 'PP') and Reader.Str(s) then
666 | Options.FPCPath := '"' + s + '"'
667 | else if (Key = 'FPCDIR') and Reader.Str(s) then
668 | Options.FPCSrcDir := '"' + s + '"'
669 | else if (Key = 'LAZARUSDIR') and Reader.Str(s) then
670 | Options.LazarusSrcDir := '"' + s + '"'
671 | else if (Key = 'FPCTARGET') and Reader.Str(s) then
672 | Options.TargetOS := s
673 | else if (Key = 'FPCTARGETCPU') and Reader.Str(s) then
674 | Options.TargetProcessor := s
675 | else if (Key = 'fpcStandardUnitsPaths') and Reader.Str(s) then
676 | StandardUnitsPaths := s
677 | else if (Key = 'projectSearchPaths') and Reader.Str(s) then
678 | ProjectSearchPaths := s
679 | else if (Key = 'engineDevMode') and Reader.Bool(b) then
680 | EngineDeveloperMode := b
681 | else if (Key = 'syntaxErrorReportingMode') and Reader.Number(i) then
682 | SyntaxErrorReportingMode := SyntaxErrorReportingModeFromInt(i);
683 | end;
684 | end;
685 |
686 | if Options.TargetOS = 'windows' then
687 | begin
688 | Options.TargetOS := {$ifdef CPUx86_64}'win64'{$else}'win32'{$endif};
689 | DebugLog('Correcting OS "windows" to "%s"', [Options.TargetOS]);
690 | end;
691 | if Options.TargetOS = '' then
692 | begin
693 | Options.TargetOS := AutoDetectOS;
694 | DebugLog('Autodetected OS as "%s"', [Options.TargetOS]);
695 | end;
696 | if Options.TargetProcessor = '' then
697 | begin
698 | Options.TargetProcessor := AutoDetectCPU;
699 | DebugLog('Autodetected CPU as "%s"', [Options.TargetProcessor]);
700 | end;
701 |
702 | // Set the root directory
703 | // TODO: Support "workspaces"
704 | URIToFilename(RootUri, Directory);
705 |
706 | // Try to fill in missing values by reading lazarus config
707 | DebugLog('', []);
708 | GuessCodeToolConfig(Options);
709 |
710 | Options.ProjectDir := Directory;
711 | Options.TestPascalFile := GetTempFileName;
712 |
713 |
714 | DebugLog('', []);
715 | DebugLog(':: Using Options', []);
716 | DebugLog(' PP = %s', [Options.FPCPath]);
717 | DebugLog(' FPCDIR = %s', [Options.FPCSrcDir]);
718 | DebugLog(' LAZARUSDIR = %s', [Options.LazarusSrcDir]);
719 | DebugLog(' FPCTARGET = %s', [Options.TargetOS]);
720 | DebugLog(' FPCTARGETCPU = %s', [Options.TargetProcessor]);
721 |
722 | DebugLog('', []);
723 | DebugLog(':: Castle Game Engine', []);
724 | ExtraOptions := ExtraFpcOptions;
725 | StandardUnitsPaths:= ParseStandardUnitsPaths(StandardUnitsPaths);
726 | ParseWorkspacePaths(ProjectSearchPaths, Directory);
727 | ProjectSearchPaths := ParseProjectSearchPaths(ProjectSearchPaths, Directory);
728 | Options.FPCOptions := Options.FPCOptions + ' ' + ExtraOptions +
729 | ' ' + StandardUnitsPaths + ' ' + ProjectSearchPaths;
730 | DebugLog(' Adding compiler extra options: ' + ExtraOptions + LineEnding);
731 | if StandardUnitsPaths <> '' then
732 | DebugLog(' Adding compiler standard Units Paths: ' + StandardUnitsPaths + LineEnding);
733 |
734 | if ProjectSearchPaths <> '' then
735 | DebugLog(' Adding project search paths: ' + ProjectSearchPaths + LineEnding);
736 |
737 | DebugLog(' Options.FPCOptions : ' + Options.FPCOptions + LineEnding);
738 |
739 | DebugLog('', []);
740 | DebugLog(':: Searching global packages', []);
741 | PopulateGlobalPackages([
742 | Options.LazarusSrcDir + '/components',
743 | Options.LazarusSrcDir + '/lcl'
744 | ]);
745 |
746 | with CodeToolBoss do
747 | begin
748 | Init(Options);
749 | IdentifierList.SortForHistory := True;
750 |
751 | { LCL changed in
752 | https://gitlab.com/freepascal.org/lazarus/lazarus/-/commit/de3a85ac41a2f882500c2d479dff48bd7bbec7f1 ,
753 | removing SortForScope (Boolean),
754 | adding instead SortMethodForCompletion (enum).
755 | The SortMethodForCompletion=icsScopedAlphabetic seems to be the equivalent
756 | of the old SortForScope=True. }
757 | {$if LCL_FULLVERSION >= 3000000}
758 | IdentifierList.SortMethodForCompletion := icsScopedAlphabetic;
759 | {$else}
760 | IdentifierList.SortForScope := True;
761 | {$endif}
762 | end;
763 |
764 | // Load packages into our internal database and resolve dependencies
765 | DebugLog('', []);
766 | DebugLog(':: Loading all packages in %s', [Directory]);
767 | LoadAllPackagesUnderPath(Directory);
768 |
769 | DebugLog('', []);
770 | DebugLog(':: Guessing missing dependencies', []);
771 | GuessMissingDepsForAllPackages(Directory);
772 |
773 | // Configure CodeTools
774 | DebugLog('', []);
775 | DebugLog(':: Configuring Paths', []);
776 | ConfigurePaths(Directory);
777 |
778 | DebugLog('', []);
779 |
780 | // Send response & announce our capabilities
781 | Response := TRpcResponse.Create(Request.Id);
782 | Writer := Response.Writer;
783 |
784 | Writer.Dict;
785 | Writer.Key('serverInfo');
786 | Writer.Dict;
787 | Writer.Key('name');
788 | Writer.Str('Pascal Language Server');
789 | Writer.DictEnd;
790 |
791 | Writer.Key('capabilities'); // https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#clientCapabilities
792 | Writer.Dict;
793 | Writer.Key('textDocumentSync'); // https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocumentSyncOptions
794 | Writer.Dict;
795 | Writer.Key('openClose');
796 | Writer.Bool(true);
797 |
798 | Writer.Key('change');
799 | Writer.Number(1); // 1 = Sync by sending full content, 2 = Incremental
800 | Writer.DictEnd;
801 |
802 | Writer.Key('completionProvider');
803 | Writer.Dict;
804 | Writer.Key('triggerCharacters');
805 | Writer.Null;
806 |
807 | Writer.Key('allCommitCharacters');
808 | Writer.Null;
809 |
810 | Writer.Key('resolveProvider');
811 | Writer.Bool(false);
812 | Writer.DictEnd;
813 |
814 | Writer.Key('signatureHelpProvider');
815 | Writer.Dict;
816 | Writer.Key('triggerCharacters');
817 | Writer.List;
818 | Writer.Str('(');
819 | Writer.Str(',');
820 | Writer.ListEnd;
821 |
822 | Writer.Key('retriggerCharacters');
823 | Writer.List;
824 | Writer.ListEnd;
825 | Writer.DictEnd;
826 |
827 | Writer.Key('declarationProvider');
828 | Writer.Bool(true);
829 |
830 | Writer.Key('definitionProvider');
831 | Writer.Bool(true);
832 |
833 | Writer.Key('documentSymbolProvider');
834 | Writer.Bool(true);
835 |
836 | Writer.Key('workspaceSymbolProvider');
837 | Writer.Bool(true);
838 | Writer.DictEnd;
839 |
840 | //Writer.Key('workspaceFolders');
841 | //Writer.Bool(true);
842 | Writer.DictEnd;
843 |
844 | Rpc.Send(Response);
845 | LogTrace(Rpc, 'Initliasasas ');
846 | finally
847 | FreeAndNil(Options);
848 | FreeAndNil(Response);
849 | end;
850 | end;
851 |
852 | end.
853 |
854 |
--------------------------------------------------------------------------------
/server/ujsonrpc.pas:
--------------------------------------------------------------------------------
1 | // Pascal Language Server
2 | // Copyright 2020 Arjan Adriaanse
3 |
4 | // This file is part of Pascal Language Server.
5 |
6 | // Pascal Language Server is free software: you can redistribute it
7 | // and/or modify it under the terms of the GNU General Public License
8 | // as published by the Free Software Foundation, either version 3 of
9 | // the License, or (at your option) any later version.
10 |
11 | // Pascal Language Server is distributed in the hope that it will be
12 | // useful, but WITHOUT ANY WARRANTY; without even the implied warranty
13 | // of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 | // GNU General Public License for more details.
15 |
16 | // You should have received a copy of the GNU General Public License
17 | // along with Pascal Language Server. If not, see
18 | // .
19 |
20 | unit ujsonrpc;
21 |
22 | {$mode objfpc}{$H+}
23 |
24 | interface
25 |
26 | uses
27 | Classes, SysUtils, ubufferedreader, jsonstream;
28 |
29 | type
30 | TRpcIdKind = (ridString, ridInteger, ridNull);
31 |
32 | TRpcRequest = class;
33 |
34 | TRpcId = record
35 | Kind: TRpcIdKind;
36 | Str: string;
37 | Int: Integer;
38 | end;
39 |
40 | TRpcRequest = class
41 | protected
42 | FBuffer: TMemoryStream;
43 | public
44 | Method: string;
45 | Id: TRpcId;
46 | Reader: TJsonReader;
47 |
48 | function AsString: string;
49 | destructor Destroy; override;
50 | end;
51 |
52 | { Send message to LSP client (response to a previous request or notification). }
53 | TRpcResponse = class
54 | private
55 | procedure InternalCreate;
56 | procedure InternalCreateId(const Id: TRpcId);
57 | protected
58 | FBuffer: TMemoryStream;
59 | FFinalized: Boolean;
60 | procedure Finalize;
61 | public
62 | Writer: TJsonWriter;
63 | constructor Create(Id: TRpcId);
64 | constructor CreateError(Id: TRpcId; Code: Integer; const Msg: string);
65 | constructor CreateRequest(const Method: string; Id: TRpcId);
66 | { Create JSON-RPC notification.
67 | Note that notifications, following json-rpc (ver 2), do not have "id"
68 | and the other side does not reply to them (see https://www.jsonrpc.org/specification#notification ). }
69 | constructor CreateNotification(const Method: string);
70 | function AsString: string;
71 | destructor Destroy; override;
72 | end;
73 |
74 | TRpcPeer = class
75 | protected
76 | FInput: TBufferedReader;
77 | FOutput: TStream;
78 | public
79 | constructor Create(Input: TStream; Output: TStream);
80 |
81 | function Receive: TRpcRequest;
82 | procedure Send(Response: TRpcResponse);
83 | end;
84 |
85 | { ERpcException }
86 |
87 | ERpcError = class(Exception)
88 | public
89 | Code: Integer;
90 | constructor Create(ACode: Integer; const Msg: string);
91 | constructor CreateFmt(
92 | ACode: Integer; const Fmt: string; args: array of const
93 | );
94 | end;
95 |
96 | const
97 | jsrpcServerNotInitialized = -32002;
98 | jsrpcParseError = -32700;
99 | jsrpcRequestCancelled = -32800;
100 | jsrpcContentModified = -32801;
101 | jsrpcInvalidRequest = -32600;
102 | jsrpcMethodNotFound = -32601;
103 | jsrpcRequestFailed = -32803;
104 |
105 | implementation
106 |
107 | uses
108 | CastleLsp, udebug;
109 |
110 | procedure WriteRpcId(Writer: TJsonWriter; const Id: TRPcId);
111 | begin
112 | case Id.Kind of
113 | ridString: Writer.Str(Id.Str);
114 | ridInteger: Writer.Number(Id.Int);
115 | else Writer.Null;
116 | end;
117 | end;
118 |
119 | { TRpcRequest }
120 |
121 | destructor TRpcRequest.Destroy;
122 | begin
123 | FreeAndNil(Reader);
124 | FreeAndNil(FBuffer);
125 | end;
126 |
127 | function TRpcRequest.AsString: string;
128 | begin
129 | SetLength(Result, FBuffer.Size);
130 | Move(PByte(FBuffer.Memory)^, Result[1], FBuffer.Size);
131 | end;
132 |
133 | { TRpcResponse }
134 |
135 | procedure TRpcResponse.InternalCreate;
136 | begin
137 | inherited Create;
138 | FBuffer := TMemoryStream.Create;
139 | Writer := TJsonWriter.Create(FBuffer);
140 | end;
141 |
142 | procedure TRpcResponse.InternalCreateId(const Id: TRpcId);
143 | begin
144 | InternalCreate;
145 | Writer.Dict;
146 | Writer.Key('jsonrpc');
147 | Writer.Str('2.0');
148 | Writer.Key('id');
149 | WriteRpcId(Writer, Id);
150 | end;
151 |
152 | constructor TRpcResponse.Create(Id: TRpcId);
153 | begin
154 | InternalCreateId(Id);
155 | Writer.Key('result');
156 | end;
157 |
158 | constructor TRpcResponse.CreateError(
159 | Id: TRpcId; Code: Integer; const Msg: string
160 | );
161 | begin
162 | InternalCreateId(Id);
163 |
164 | Writer.Key('error');
165 | Writer.Dict;
166 | Writer.Key('code');
167 | Writer.Number(Code);
168 |
169 | Writer.Key('message');
170 | Writer.Str(Msg);
171 | Writer.DictEnd;
172 | end;
173 |
174 | constructor TRpcResponse.CreateRequest(const Method: string; Id: TRpcId);
175 | begin
176 | InternalCreateId(Id);
177 | Writer.Key('method');
178 | Writer.Str(Method);
179 | end;
180 |
181 | constructor TRpcResponse.CreateNotification(const Method: string);
182 | begin
183 | InternalCreate;
184 | Writer.Dict;
185 | Writer.Key('jsonrpc');
186 | Writer.Str('2.0');
187 | Writer.Key('method');
188 | Writer.Str(Method);
189 | end;
190 |
191 | destructor TRpcResponse.Destroy;
192 | begin
193 | FreeAndNil(Writer);
194 | FreeAndNil(FBuffer);
195 | inherited;
196 | end;
197 |
198 | procedure TRpcResponse.Finalize;
199 | begin
200 | if not FFinalized then
201 | Writer.DictEnd;
202 | FFinalized := true;
203 | end;
204 |
205 | function TRpcResponse.AsString: string;
206 | begin
207 | SetLength(Result, FBuffer.Size);
208 | Move(PByte(FBuffer.Memory)^, Result[1], FBuffer.Size);
209 | end;
210 |
211 | { TRpcPeer }
212 |
213 | constructor TRpcPeer.Create(Input: TStream; Output: TStream);
214 | begin
215 | FInput := TBufferedReader.Create(Input);;
216 | FOutput := Output;
217 | end;
218 |
219 | function TRpcPeer.Receive: TRpcRequest;
220 | var
221 | Buffer: TMemoryStream;
222 | Reader: TJsonReader;
223 | Header, Key, Val: string;
224 | Idx, Len: Integer;
225 |
226 | Version: string;
227 | Method: string;
228 | Id: TRpcId;
229 |
230 | CutLength: Integer;
231 | LogFullJson: Boolean;
232 | begin
233 | Result := nil;
234 | Buffer := nil;
235 | Reader := nil;
236 |
237 | try
238 | Header := FInput.ReadLine;
239 | if Header = '' then
240 | exit;
241 |
242 | Len := 0;
243 | while Header <> '' do
244 | begin
245 | Idx := Pos(':', Header);
246 | Key := Copy(Header, 1, Idx - 1);
247 | Delete(Header, 1, Idx);
248 | Val := Trim(Header);
249 | if Key = 'Content-Length' then
250 | Len := StrToInt(Val);
251 | Header := FInput.ReadLine;
252 | end;
253 |
254 | if Len = 0 then
255 | raise EParserError.Create('Invalid request body.');
256 |
257 | Buffer := TBytesStream.Create();
258 | Buffer.SetSize(Len);
259 | FInput.BlockRead(PByte(Buffer.Memory)^, Len);
260 |
261 | // 1st pass: Extract meta data
262 | Reader := TJsonReader.Create(Buffer);
263 |
264 | if Reader.Dict then
265 | while (Reader.Advance <> jsDictEnd) and Reader.key(Key) do
266 | begin
267 | if Key = 'jsonrpc' then
268 | Reader.Str(Version)
269 | else if Key = 'method' then
270 | Reader.Str(Method)
271 | else if (Key = 'id') and Reader.Str(Id.Str) then
272 | Id.Kind := ridString
273 | else if (Key = 'id') and Reader.Number(Id.Int) then
274 | Id.Kind := ridInteger
275 | else if (Key = 'id') and Reader.Null then
276 | Id.Kind := ridNull;
277 | end;
278 |
279 | if Reader.LastError <> jeNoError then
280 | raise ERpcError.CreateFmt(
281 | jsrpcParseError,
282 | 'Invalid Request. JSON error @%d: %s',
283 | [Reader.LastErrorPosition, Reader.LastErrorMessage]
284 | );
285 |
286 | if (Version <> '2.0') then
287 | raise ERpcError.Create(
288 | jsrpcInvalidRequest,
289 | 'No or invalid jsonrpc version specified. Must be 2.0.'
290 | );
291 |
292 | if (Method = '') then
293 | raise ERpcError.Create(
294 | jsrpcInvalidRequest,
295 | 'No method specified.'
296 | );
297 |
298 | FreeAndNil(Reader);
299 |
300 | // 2nd pass: Seek to params
301 | Buffer.Position := 0;
302 | Reader := TJsonReader.Create(Buffer);
303 | if Reader.Dict then
304 | while Reader.Advance <> jsDictEnd do
305 | if Reader.Key(Key) and (Key = 'params') then
306 | break;
307 |
308 | // Workaround if no params were supplied (probably unnecessary)
309 | if Reader.State = jsEOF then
310 | begin
311 | FreeAndNil(Reader);
312 | FreeAndNil(Buffer);
313 | Buffer := TStringStream.Create('null');
314 | Reader := TJsonReader.Create(Buffer);
315 | end;
316 |
317 | Result := TRpcRequest.Create;
318 | Result.Method := Method;
319 | Result.Id := Id;
320 | Result.Reader := Reader;
321 | Result.FBuffer := Buffer;
322 |
323 | LogFullJson := UserConfig.ReadBool('log', 'full_json', false);
324 | if LogFullJson then
325 | CutLength := MaxInt
326 | else
327 | CutLength := 2000;
328 |
329 | DebugLog('> Request: ' + LineEnding + '%s', [
330 | // Use TrimRight as the Response may contain some newline in undefined convention (Unix or Windows)
331 | TrimRight(Copy(Result.AsString, 1, CutLength))
332 | ]);
333 | except
334 | FreeAndNil(Result);
335 | FreeAndNil(Reader);
336 | FreeAndNil(Buffer);
337 | end;
338 | end;
339 |
340 | procedure TRpcPeer.Send(Response: TRpcResponse);
341 | const
342 | ContentType: string = 'application/vscode-jsonrpc; charset=utf-8';
343 | procedure WriteString(const S: string);
344 | begin
345 | FOutput.WriteBuffer(S[1], Length(S) * sizeof(S[1]));
346 | end;
347 | var
348 | CutLength: Integer;
349 | LogFullJson: Boolean;
350 | begin
351 | Response.Finalize;
352 |
353 | WriteString(Format(
354 | 'Content-Type: %s'#13#10'Content-Length:%d'#13#10#13#10,
355 | [ContentType, Response.FBuffer.Size]
356 | ));
357 | FOutput.WriteBuffer(
358 | PByte(Response.FBuffer.Memory)^,
359 | Response.FBuffer.Size
360 | );
361 |
362 | if FOutput is THandleStream then
363 | FileFlush(THandleStream(FOutput).Handle);
364 |
365 | LogFullJson := UserConfig.ReadBool('log', 'full_json', false);
366 | if LogFullJson then
367 | CutLength := MaxInt
368 | else
369 | CutLength := 2000;
370 |
371 | DebugLog('< Response: ' + LineEnding + '%s', [
372 | // Use TrimRight as the Response may contain some newline in undefined convention (Unix or Windows)
373 | TrimRight(Copy(Response.AsString, 1, CutLength))
374 | ]);
375 | end;
376 |
377 | constructor ERpcError.Create(ACode: Integer; const Msg: string);
378 | begin
379 | inherited Create(Msg);
380 | Code := ACode;
381 | end;
382 |
383 | constructor ERpcError.CreateFmt(
384 | ACode: Integer; const Fmt: string; args: array of const
385 | );
386 | begin
387 | inherited CreateFmt(Fmt, args);
388 | Code := ACode;
389 | end;
390 |
391 | end.
392 |
393 |
--------------------------------------------------------------------------------
/server/ulogvscode.pas:
--------------------------------------------------------------------------------
1 | unit ULogVSCode;
2 |
3 | {$mode ObjFPC}{$H+}
4 |
5 | interface
6 |
7 | uses
8 | sysutils, jsonstream, ujsonrpc;
9 |
10 | type
11 |
12 | TVSCodeTraceValue = (
13 | tvOff = 0,
14 | tvMessages,
15 | tvVerbose
16 | );
17 |
18 | { https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#messageType }
19 | TVSCodeMessageLogType = (
20 | mltError = 1,
21 | mltWarning,
22 | mltInfo,
23 | mltLog
24 | );
25 |
26 | function ParseSetTrace(const Request: TRpcRequest): TVSCodeTraceValue;
27 | { Log Trace to work you need to add :
28 | "pascal-language-server.trace.server": "verbose",
29 | "pascal-language-server.trace.server": "messages",
30 | to your settings.json.
31 |
32 | Default value is "pascal-language-server.trace.server": "off". }
33 | procedure LogTrace(const Rpc: TRpcPeer; const Message: String; const Verbose: String = '');
34 |
35 | { https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#window_logMessage}
36 | procedure LogError(const Rpc: TRpcPeer; const Message: String);
37 | procedure LogWarning(const Rpc: TRpcPeer; const Message: String);
38 | procedure LogInfo(const Rpc: TRpcPeer; const Message: String);
39 | procedure LogMessage(const Rpc: TRpcPeer; const LogType: TVSCodeMessageLogType; const Message: String);
40 |
41 | var
42 | TraceValue: TVSCodeTraceValue = tvOff;
43 |
44 |
45 | implementation
46 |
47 | uses udebug;
48 |
49 | { https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#traceValue }
50 | function ParseSetTrace(const Request: TRpcRequest): TVSCodeTraceValue;
51 | var
52 | Reader: TJsonReader;
53 | Key: String;
54 | Value: String;
55 | begin
56 | Reader := Request.Reader;
57 |
58 | if Reader.Dict then
59 | begin
60 | while (Reader.Advance <> jsDictEnd) and Reader.Key(Key) do
61 | begin
62 | if Key = 'value' then
63 | begin
64 | Reader.Str(Value);
65 |
66 | if Value = 'off' then
67 | Exit(tvOff)
68 | else
69 | if Value = 'message' then
70 | Exit(tvMessages)
71 | else
72 | if Value = 'verbose' then
73 | Exit(tvVerbose)
74 | else
75 | begin
76 | DebugLog('ParseSetTrace uknown value: ' + Value);
77 | Exit(tvVerbose);
78 | end;
79 | end;
80 | end;
81 | end;
82 | Result := tvOff;
83 | end;
84 |
85 | { https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#logTrace }
86 | procedure LogTrace(const Rpc: TRpcPeer; const Message: String; const Verbose: String = '');
87 | var
88 | Notif: TRpcResponse;
89 | Writer: TJsonWriter;
90 | begin
91 | if TraceValue = tvOff then
92 | Exit;
93 |
94 | Notif := TRpcResponse.CreateNotification('$/logTrace');
95 | try
96 | Writer := Notif.Writer;
97 | Writer.Key('params');
98 | Writer.Dict;
99 | Writer.Key('message');
100 | Writer.Str(Message);
101 | if TraceValue = tvVerbose then
102 | begin
103 | Writer.Key('verbose');
104 | Writer.Str(Verbose);
105 | end;
106 | Writer.DictEnd;
107 | Rpc.Send(Notif);
108 | finally
109 | FreeAndNil(Notif);
110 | end;
111 | end;
112 |
113 | procedure LogError(const Rpc: TRpcPeer; const Message: String);
114 | begin
115 | LogMessage(Rpc, mltError, Message);
116 | end;
117 |
118 | procedure LogWarning(const Rpc: TRpcPeer; const Message: String);
119 | begin
120 | LogMessage(Rpc, mltWarning, Message);
121 | end;
122 |
123 | procedure LogInfo(const Rpc: TRpcPeer; const Message: String);
124 | begin
125 | LogMessage(Rpc, mltInfo, Message);
126 | end;
127 |
128 | procedure LogMessage(const Rpc: TRpcPeer; const LogType: TVSCodeMessageLogType; const Message: String);
129 | var
130 | Notif: TRpcResponse;
131 | Writer: TJsonWriter;
132 | begin
133 | Notif := TRpcResponse.CreateNotification('window/logMessage');
134 | try
135 | Writer := Notif.Writer;
136 | Writer.Key('params');
137 | Writer.Dict;
138 | Writer.Key('type');
139 | Writer.Number(Integer(LogType));
140 |
141 | Writer.Key('message');
142 | Writer.Str(Message);
143 | Writer.DictEnd;
144 | Rpc.Send(Notif);
145 | finally
146 | FreeAndNil(Notif);
147 | end;
148 | end;
149 |
150 |
151 | end.
152 |
153 |
--------------------------------------------------------------------------------
/server/upackages.pas:
--------------------------------------------------------------------------------
1 | // Pascal Language Server
2 | // Copyright 2021 Philip Zander
3 |
4 | // This file is part of Pascal Language Server.
5 |
6 | // Pascal Language Server is free software: you can redistribute it
7 | // and/or modify it under the terms of the GNU General Public License
8 | // as published by the Free Software Foundation, either version 3 of
9 | // the License, or (at your option) any later version.
10 |
11 | // Pascal Language Server is distributed in the hope that it will be
12 | // useful, but WITHOUT ANY WARRANTY; without even the implied warranty
13 | // of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 | // GNU General Public License for more details.
15 |
16 | // You should have received a copy of the GNU General Public License
17 | // along with Pascal Language Server. If not, see
18 | // .
19 |
20 | unit upackages;
21 |
22 | {$mode objfpc}{$H+}
23 |
24 | { This unit uses FPC XML units, that have DOMString = UnicodeString,
25 | and gets/sets strings as AnsiString. This causes warnings:
26 |
27 | server/upackages.pas(167,38) Warning: (4104) Implicit string type conversion from "AnsiString" to "UnicodeString"
28 | server/upackages.pas(173,14) Warning: (4105) Implicit string type conversion with potential data loss from "UnicodeString" to "AnsiString"
29 | server/upackages.pas(239,26) Warning: (4105) Implicit string type conversion with potential data loss from "UnicodeString" to "AnsiString"
30 | server/upackages.pas(253,56) Warning: (4105) Implicit string type conversion with potential data loss from "UnicodeString" to "AnsiString"
31 |
32 | TODO: Make sure it is really OK to ignore these warnings,
33 | by making sure AnsiStrings contain UTF-8 on all platforms,
34 | like Lazarus LCL and Castle Game Engine do.
35 | And making sure we have WideString manager or such initialized. }
36 | {$warn 4104 off}
37 | {$warn 4105 off}
38 |
39 | interface
40 |
41 | type
42 | TPaths = record
43 | // Search path for units (OtherUnitFiles)
44 | UnitPath: string;
45 | // Search path for includes (IncludeFiles)
46 | IncludePath: string;
47 | // Additional sources, not passed to compiler (SrcFiles)
48 | SrcPath: string;
49 | end;
50 |
51 | TPackage = class;
52 |
53 | TDependency = record
54 | // Name of the package, e.g. 'LCLBase'
55 | Name: string;
56 |
57 | // Projects may hardcode a path to a package. If a path was hardcoded, Path
58 | // will contain the expanded path, otherwise will be empty string.
59 | Path: string;
60 |
61 | // Whether the hardcoded path should take precedence over a global package
62 | // of the same name.
63 | Prefer: Boolean;
64 |
65 | // Once we have resolved the dependency, we cache a reference to the package
66 | // here:
67 | Package: TPackage;
68 | end;
69 |
70 | { TPackage }
71 |
72 | TPackage = class
73 | // Name of the package / project
74 | //Name: string;
75 | PkgFile: string;
76 |
77 | // Home directory of the package / project
78 | Dir: string;
79 |
80 | // Valid: True if the package was found, False otherwise. If False, this
81 | // is a dummy object whose only purpose is to prevent us from trying to load
82 | // a non-existing package multiple times.
83 | Valid: Boolean;
84 |
85 | // The search path resolution process involves several stages:
86 | // 0. Compile all 1st party search paths defined in the package file and
87 | // store them in "Paths".
88 | // 1. Resolve the dependencies (find file name for a given package name)
89 | // 2. Compile the search paths for all dependencies and add them to our own
90 | // search paths (Resolved Paths)
91 | // 3. Announce the search paths for the package home directory to
92 | // CodeToolBoss.
93 |
94 | DidResolveDeps: Boolean; // True after step 1 is completed
95 | DidResolvePaths: Boolean; // True after step 2 is completed
96 | Configured: Boolean; // True after step 3 is completed
97 |
98 | Visited: Boolean; // Temporary flag while guessing dependencies.
99 |
100 | // Absolute 1st-degree search paths for this package
101 | Paths: TPaths;
102 |
103 | // List of dependencies of this package
104 | Dependencies: array of TDependency;
105 |
106 | // List of packages requiring this package
107 | // (only 1st degree dependencies)
108 | RequiredBy: array of TPackage;
109 |
110 | // Search paths including dependencies
111 | ResolvedPaths: TPaths;
112 |
113 | constructor Create;
114 | end;
115 |
116 | // Get package or project information from a file. The file must end in .lpk
117 | // if it is a package, or .lpi if it is a project.
118 | // Results are cached. If the file could not be loaded, the Valid member of
119 | // the result will be set to False.
120 | function GetPackageOrProject(const FileName: string): TPackage;
121 |
122 | // Get the location of a global package by its name.
123 | // E.g. 'LCLBase' -> '/Applications/Lazarus/lcl/lclbase.lpk'
124 | function LookupGlobalPackage(const Name: string): string;
125 |
126 | procedure PopulateGlobalPackages(const SearchPaths: array of string);
127 |
128 | implementation
129 |
130 | uses
131 | Classes, SysUtils, contnrs, FileUtil, LazFileUtils, DOM, XMLRead, uutils,
132 | udebug;
133 |
134 | var
135 | PkgNameToPath: TFPStringHashTable;
136 | // Map Path -> TPackage
137 | PkgCache: TFPObjectHashTable;
138 |
139 |
140 | procedure PopulateGlobalPackages(const SearchPaths: array of string);
141 | var
142 | Files: TStringList;
143 | Dir, FileName, Name: string;
144 | begin
145 | Files := TStringList.Create;
146 | try
147 | for Dir in SearchPaths do
148 | begin
149 | DebugLog(' PopulateGlobalPackages search path ' + Dir + LineEnding);
150 | DebugLog(' %s/*.lpk', [Dir]);
151 | FindAllFiles(Files, Dir, '*.lpk');
152 | end;
153 |
154 | for FileName in Files do
155 | begin
156 | Name := ExtractFileNameOnly(FileName);
157 | DebugLog(' Global Package: ' + UpperCase(Name) + ' :' + FileName + LineEnding);
158 | PkgNameToPath[UpperCase(Name)] := FileName;
159 | end;
160 | DebugLog(' Found %d packages', [Files.Count]);
161 | finally
162 | Files.Free;
163 | end;
164 | end;
165 |
166 | procedure LoadPackageOrProject(const FileName: string);
167 | var
168 | Doc: TXMLDocument;
169 | Root: TDomNode;
170 | Package: TPackage;
171 |
172 | function GetAdditionalPaths(
173 | SearchPaths: TDomNode; const What: string
174 | ): String;
175 | var
176 | Node: TDomNode;
177 | Segments: TStringArray;
178 | S, Segment, AbsSegment: string;
179 | begin
180 | Result := '';
181 |
182 | Node := SearchPaths.FindNode(What);
183 | if Assigned(Node) then
184 | Node := Node.Attributes.GetNamedItem('Value');
185 | if not Assigned(Node) then
186 | Exit;
187 |
188 | S := Node.NodeValue;
189 | Segments := S.Split([';'], TStringSplitOptions.ExcludeEmpty);
190 |
191 | for Segment in Segments do
192 | begin
193 | AbsSegment := CreateAbsolutePath(Segment, Package.Dir);
194 | Result := Result + ';' + AbsSegment;
195 | end;
196 | end;
197 |
198 | procedure LoadPaths;
199 | var
200 | CompilerOptions, SearchPaths: TDomNode;
201 | begin
202 | Package.Paths.IncludePath := Package.Dir;
203 | Package.Paths.UnitPath := Package.Dir;
204 |
205 | CompilerOptions := Root.FindNode('CompilerOptions');
206 | if not Assigned(CompilerOptions) then
207 | Exit;
208 |
209 | SearchPaths := CompilerOptions.FindNode('SearchPaths');
210 | if not Assigned(SearchPaths) then
211 | Exit;
212 |
213 | Package.Paths.IncludePath := MergePaths([
214 | Package.Paths.IncludePath,
215 | GetAdditionalPaths(SearchPaths, 'IncludeFiles')
216 | ]);
217 | Package.Paths.UnitPath := MergePaths([
218 | Package.Paths.UnitPath,
219 | GetAdditionalPaths(SearchPaths, 'OtherUnitFiles')
220 | ]);
221 | Package.Paths.SrcPath := GetAdditionalPaths(SearchPaths, 'SrcPath');
222 | end;
223 |
224 | procedure LoadDeps;
225 | var
226 | Deps, Item, Name,
227 | Path, Prefer: TDomNode;
228 | Dep: TDependency;
229 | i, DepCount: Integer;
230 | begin
231 | if UpperCase(ExtractFileExt(FileName)) = '.LPK' then
232 | Deps := Root.FindNode('RequiredPkgs')
233 | else
234 | Deps := Root.FindNode('RequiredPackages');
235 |
236 | if not Assigned(Deps) then
237 | Exit;
238 |
239 | DepCount := 0;
240 | SetLength(Package.Dependencies, Deps.ChildNodes.Count);
241 |
242 | for i := 0 to Deps.ChildNodes.Count - 1 do
243 | begin
244 | Item := Deps.ChildNodes.Item[i];
245 |
246 | Name := Item.FindNode('PackageName');
247 | if not Assigned(Name) then
248 | continue;
249 |
250 | Name := Name.Attributes.GetNamedItem('Value');
251 | if not Assigned(Name) then
252 | continue;
253 |
254 | Dep.Name := Name.NodeValue;
255 | Dep.Prefer := False;
256 | Dep.Package := nil;
257 | Dep.Path := '';
258 |
259 | Path := Item.FindNode('DefaultFilename');
260 |
261 | if Assigned(Path) then
262 | begin
263 | Prefer := Path.Attributes.GetNamedItem('Prefer');
264 | Path := Path.Attributes.GetNamedItem('Value');
265 |
266 | Dep.Prefer := Assigned(Prefer) and (Prefer.NodeValue = 'True');
267 | if Assigned(Path) then
268 | Dep.Path := CreateAbsolutePath(Path.NodeValue, Package.Dir);
269 |
270 | //DebugLog('HARDCODED DEP %s in %s', [Dep.Name, Dep.Path]);
271 | //DebugLog(' Dir: %s, Rel: %s', [Package.Dir, Path.NodeValue]);
272 | end;
273 |
274 | Package.Dependencies[DepCount] := Dep;
275 | Inc(DepCount);
276 | end;
277 | end;
278 |
279 | begin
280 | if Assigned(PkgCache[FileName]) then
281 | Exit;
282 |
283 | DebugLog('Loading %s', [FileName]);
284 |
285 | Package := TPackage.Create;
286 | Package.Valid := False;
287 | Package.Dir := ExtractFilePath(FileName);
288 | Package.PkgFile := FileName;
289 |
290 | PkgCache[FileName] := Package;
291 |
292 | try
293 | try
294 | ReadXMLFile(doc, filename);
295 |
296 | Root := Doc.DocumentElement;
297 | if Root.NodeName <> 'CONFIG' then
298 | Exit;
299 |
300 | if UpperCase(ExtractFileExt(FileName)) = '.LPK' then
301 | Root := Root.FindNode('Package')
302 | else
303 | Root := Root.FindNode('ProjectOptions');
304 |
305 | if not Assigned(Root) then
306 | Exit;
307 |
308 | LoadPaths;
309 | LoadDeps;
310 |
311 | Package.Valid := True;
312 | except on E:Exception do
313 | // swallow
314 | DebugLog('Error loading %s: %s', [FileName, E.Message]);
315 | end;
316 | finally
317 | if Assigned(doc) then
318 | FreeAndNil(doc);
319 | end;
320 | end;
321 |
322 | function GetPackageOrProject(const FileName: String): TPackage;
323 | begin
324 | Result := TPackage(PkgCache[FileName]);
325 | if not Assigned(Result) then
326 | begin
327 | LoadPackageOrProject(FileName);
328 | Result := TPackage(PkgCache[FileName]);
329 | end;
330 | end;
331 |
332 | function LookupGlobalPackage(const Name: String): String;
333 | begin
334 | Result := PkgNameToPath[UpperCase(Name)];
335 | end;
336 |
337 | { TPackage }
338 |
339 | constructor TPackage.Create;
340 | begin
341 | Valid := False;
342 | Configured := False;
343 | DidResolvePaths := False;
344 | DidResolveDeps := False;
345 | end;
346 |
347 | initialization
348 |
349 | PkgNameToPath := TFPStringHashTable.Create;
350 | PkgCache := TFPObjectHashTable.Create;
351 | //PopulateGlobalPackages;
352 |
353 |
354 | end.
355 |
356 |
--------------------------------------------------------------------------------
/server/ushutdown.pas:
--------------------------------------------------------------------------------
1 | unit ushutdown;
2 |
3 | {$mode ObjFPC}{$H+}
4 |
5 | interface
6 |
7 | uses
8 | jsonstream, ujsonrpc;
9 |
10 | procedure Shutdown(Rpc: TRpcPeer; Request: TRpcRequest);
11 |
12 | var
13 | WasShutdown: Boolean;
14 |
15 | implementation
16 |
17 | uses SysUtils, Classes, udebug;
18 |
19 | {
20 | When client wants to stop lsp server send shutdown message and after
21 | it get response (null). Send second message exit to simply close the server.
22 |
23 | https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#shutdown
24 | }
25 | procedure Shutdown(Rpc: TRpcPeer; Request: TRpcRequest);
26 | var
27 | Response: TRpcResponse;
28 | Writer: TJsonWriter;
29 |
30 | begin
31 | DebugLog('Get shutdown message, waiting for exit...');
32 | WasShutdown := true;
33 |
34 | Response := nil;
35 | try
36 | Response := TRpcResponse.Create(Request.Id);
37 | Writer := Response.Writer;
38 |
39 | Writer.Null;
40 | Rpc.Send(Response);
41 | finally
42 | FreeAndNil(Response);
43 | end;
44 | end;
45 |
46 | end.
47 |
48 |
--------------------------------------------------------------------------------
/server/utextdocument.pas:
--------------------------------------------------------------------------------
1 | // Pascal Language Server
2 | // Copyright 2020 Arjan Adriaanse
3 | // 2021 Philip Zander
4 |
5 | // This file is part of Pascal Language Server.
6 |
7 | // Pascal Language Server is free software: you can redistribute it
8 | // and/or modify it under the terms of the GNU General Public License
9 | // as published by the Free Software Foundation, either version 3 of
10 | // the License, or (at your option) any later version.
11 |
12 | // Pascal Language Server is distributed in the hope that it will be
13 | // useful, but WITHOUT ANY WARRANTY; without even the implied warranty
14 | // of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 | // GNU General Public License for more details.
16 |
17 | // You should have received a copy of the GNU General Public License
18 | // along with Pascal Language Server. If not, see
19 | // .
20 |
21 | unit utextdocument;
22 |
23 | {$mode objfpc}{$H+}
24 |
25 | interface
26 |
27 | uses
28 | jsonstream, ujsonrpc;
29 |
30 | type
31 | TSyntaxErrorReportingMode = (
32 | sermShowMessage = 0,
33 | sermFakeCompletionItem = 1,
34 | sermErrorResponse = 2
35 | );
36 |
37 | TIdentifierCodeCompletionStyle = (
38 | ccsShowIdentifierWithParametersAndOverloads,
39 | ccsShowOnlyUniqueIdentifier
40 | );
41 |
42 | var
43 | SyntaxErrorReportingMode: TSyntaxErrorReportingMode = sermShowMessage;
44 | IdentifierCodeCompletionStyle: TIdentifierCodeCompletionStyle = ccsShowIdentifierWithParametersAndOverloads;
45 |
46 | procedure TextDocument_DidOpen(Rpc: TRpcPeer; Request: TRpcRequest);
47 | procedure TextDocument_DidChange(Rpc: TRpcPeer; Request: TRpcRequest);
48 | procedure TextDocument_SignatureHelp(Rpc: TRpcPeer; Request: TRpcRequest);
49 | procedure TextDocument_Completion(Rpc: TRpcPeer; Request: TRpcRequest);
50 | procedure TextDocument_Declaration(Rpc: TRpcPeer; Request: TRpcRequest);
51 | procedure TextDocument_Definition(Rpc: TRpcPeer; Request: TRpcRequest);
52 |
53 | implementation
54 |
55 | uses
56 | Classes, SysUtils, URIParser, CodeToolManager, CodeCache, IdentCompletionTool,
57 | BasicCodeTools, PascalParserTool, CodeTree, FindDeclarationTool, LinkScanner,
58 | CustomCodeTool, udebug, uutils, ULogVSCode;
59 |
60 | function ParseChangeOrOpen(
61 | Reader: TJsonReader; out Uri: string; out Content: string; IsChange: Boolean
62 | ): Boolean;
63 | var
64 | Key: string;
65 | HaveUri, HaveContent: Boolean;
66 | begin
67 | HaveUri := false;
68 | HaveContent := false;
69 | if Reader.Dict then
70 | while (Reader.Advance <> jsDictEnd) and Reader.Key(Key) do
71 | begin
72 | if (Key = 'textDocument') and Reader.Dict then
73 | while (Reader.Advance <> jsDictEnd) and Reader.Key(Key) do
74 | begin
75 | if (Key = 'uri') and Reader.Str(Uri) then
76 | HaveUri := true
77 | else if not IsChange and (Key = 'text') and Reader.Str(Content) then
78 | HaveContent := true;
79 | end
80 | else if IsChange and (Key = 'contentChanges') and Reader.List then
81 | while Reader.Advance <> jsListEnd do
82 | begin
83 | if Reader.Dict then
84 | while (Reader.Advance <> jsDictEnd) and (Reader.Key(Key)) do
85 | begin
86 | if (Key = 'text') and Reader.Str(Content) then
87 | HaveContent := true;
88 | end;
89 | end;
90 | end;
91 | Result := HaveUri and HaveContent;
92 | end;
93 |
94 | procedure TextDocument_DidOpen(Rpc: TRpcPeer; Request: TRpcRequest);
95 | var
96 | Code: TCodeBuffer;
97 | UriStr: string;
98 | Content, FileName: string;
99 | begin
100 | if ParseChangeOrOpen(Request.Reader, UriStr, Content, false) then
101 | begin
102 | FileName := URIToFileNameEasy(UriStr);
103 | Code := CodeToolBoss.LoadFile(FileName, false, false);
104 | { When we can't found file try to create it, workaround for creating
105 | new source files in vscode }
106 | if Code = nil then
107 | Code := CodeToolBoss.CreateFile(FileName);
108 | if Code = nil then
109 | raise ERpcError.CreateFmt(
110 | jsrpcInvalidRequest,
111 | 'Unable to load file: %s', [FileName]
112 | );
113 | Code.Source := Content;
114 | end;
115 | end;
116 |
117 | procedure TextDocument_DidChange(Rpc: TRpcPeer; Request: TRpcRequest);
118 | var
119 | Code: TCodeBuffer;
120 | UriStr: string;
121 | Content, FileName: string;
122 | begin
123 | if ParseChangeOrOpen(Request.Reader, UriStr, Content, true) then
124 | begin
125 | FileName := URIToFileNameEasy(UriStr);
126 | Code := CodeToolBoss.FindFile(FileName);
127 | if Code = nil then
128 | raise ERpcError.CreateFmt(
129 | jsrpcInvalidRequest,
130 | 'Unable to load file: %s', [FileName]
131 | );
132 | Code.Source := Content;
133 | end;
134 | end;
135 |
136 | type
137 | TStringSlice = record
138 | a, b: Integer;
139 | end;
140 |
141 | TCompletionRec = record
142 | Text: String;
143 | Identifier: TStringSlice;
144 | ResultType: TStringSlice;
145 | Parameters: array of TStringSlice;
146 | Desc: String; // TCodeTreeNodeDesc as string
147 | IdentifierType: TCodeTreeNodeDesc;
148 | end;
149 |
150 | TCompletionCallback =
151 | procedure (const Rec: TCompletionRec; Writer: TJsonWriter);
152 |
153 | {
154 | Gets completion records for curent position in code buffer and specified prefix.
155 | Parameters:
156 | Prefix - thing to search
157 | Exact - only exact identifier (for procedure signature)
158 | IncludeKeywords - include keywords in records
159 | OnlyUnique - only one per overloaded functions
160 | Callback - callback function to use (code completion or signature hint)
161 | Writer - json writer used by callback }
162 | procedure GetCompletionRecords(
163 | Code: TCodeBuffer; X, Y: Integer; const Prefix: string;
164 | const Exact, IncludeKeywords, OnlyUnique: Boolean;
165 | Callback: TCompletionCallback; Writer: TJsonWriter
166 | );
167 | var
168 | Identifier: TIdentifierListItem;
169 | i, j, Count: Integer;
170 | ResultType: string;
171 | Segment: string;
172 | node, paramsNode: TCodeTreeNode;
173 | childNode : TCodeTreeNode;
174 | SegmentLen: Integer;
175 | Rec: TCompletionRec;
176 | CodeTool: TCodeTool;
177 | CodeTreeNode: TCodeTreeNode;
178 | UniqueCheckStringList: TStringList;
179 |
180 | function AppendString(var S: string; Suffix: string): TStringSlice;
181 | begin
182 | Result.a := Length(S) + 1;
183 | Result.b := Length(S) + Length(Suffix) + 1;
184 | S := S + Suffix;
185 | end;
186 |
187 | begin
188 | assert(Code <> nil);
189 |
190 | { At first we have to check the file has unit , without that
191 | do not try return code completion because it returns only errors. }
192 | CodeToolBoss.Explore(Code, CodeTool, false, false);
193 |
194 | { This happens when opening include file without MainUnit,
195 | like https://github.com/castle-engine/castle-engine/blob/master/src/common_includes/castleconf.inc .
196 | Return empty response. }
197 | if CodeTool = nil then
198 | Exit;
199 |
200 | if CodeTool.Tree = nil then
201 | raise ERpcError.Create(jsrpcRequestFailed, 'Code tool tree is nil.');
202 |
203 | { This check fails when pas file is empty, return empty response }
204 | if CodeTool.Tree.Root = nil then
205 | Exit;
206 |
207 | { Next we have to check there is interface in the code if not
208 | hint only interface word, without this check code completion returns
209 | only "Line ..." errors when CodeToolBoss.GatherIdentifiers() is called }
210 | CodeTreeNode := CodeTool.FindInterfaceNode;
211 | if CodeTreeNode = nil then
212 | begin
213 | Rec.Text := 'interface';
214 | Rec.Identifier.a := 0;
215 | Rec.Identifier.b := 0;
216 | Rec.ResultType.a := 0;
217 | Rec.ResultType.b := 0;
218 | Rec.Parameters := nil;
219 | Rec.Desc := '';
220 | Rec.IdentifierType := ctnNone;
221 | Callback(Rec, Writer);
222 | Exit;
223 | end;
224 |
225 | { Main code completion code }
226 | CodeToolBoss.IdentifierList.Prefix := Prefix;
227 | CodeToolBoss.IdentComplIncludeKeywords := IncludeKeywords;
228 |
229 | if not CodeToolBoss.GatherIdentifiers(Code, X, Y) then
230 | raise ERpcError.Create(
231 | jsrpcRequestFailed,
232 | PositionForErrorPrefix(CodeToolBoss) + CodeToolBoss.ErrorMessage);
233 |
234 | Count := CodeToolBoss.IdentifierList.GetFilteredCount;
235 |
236 | if OnlyUnique then
237 | UniqueCheckStringList := TStringList.Create
238 | else
239 | UniqueCheckStringList := nil;
240 |
241 | try
242 | for i := 0 to Count - 1 do
243 | begin
244 | Identifier := CodeToolBoss.IdentifierList.FilteredItems[i];
245 |
246 | Rec.Text := '';
247 | Rec.Identifier.a := 0;
248 | Rec.Identifier.b := 0;
249 | Rec.ResultType.a := 0;
250 | Rec.ResultType.b := 0;
251 | Rec.Parameters := nil;
252 | Rec.Desc := '';
253 | Rec.IdentifierType := ctnNone;
254 | ResultType := '';
255 |
256 | if OnlyUnique then
257 | begin
258 | if UniqueCheckStringList.IndexOf(Identifier.Identifier) <> -1 then
259 | continue;
260 | UniqueCheckStringList.Add(Identifier.Identifier);
261 | end;
262 |
263 | if (not Exact) or (CompareText(Identifier.Identifier, Prefix) = 0) then
264 | begin
265 | paramsNode := Identifier.Tool.GetProcParamList(identifier.Node);
266 | if Assigned(paramsNode) then
267 | begin
268 | ResultType :=
269 | Identifier.Tool.ExtractProcHead(
270 | identifier.Node,
271 | [
272 | phpWithoutName, phpWithoutParamList, phpWithoutSemicolon,
273 | phpWithResultType, phpWithoutBrackets, phpWithoutGenericParams,
274 | phpWithoutParamTypes
275 | ]
276 | ).Replace(':', '').Trim;
277 |
278 | node := paramsNode.firstChild;
279 |
280 | Rec.Identifier := AppendString(Rec.Text, Identifier.Identifier);
281 | AppendString(Rec.Text, ' (');
282 |
283 | SetLength(Rec.Parameters, paramsNode.ChildCount);
284 |
285 | for j := 0 to paramsNode.ChildCount - 1 do
286 | begin
287 | Segment := Identifier.Tool.ExtractNode(node, []);
288 | Segment := StringReplace(Segment, ':', ': ', [rfReplaceAll]);
289 | Segment := StringReplace(Segment, '=', ' = ', [rfReplaceAll]);
290 |
291 | Rec.Parameters[j] := AppendString(Rec.Text, Segment);
292 |
293 | SegmentLen := Pos(':', Segment) - 1;
294 | if SegmentLen <= 0 then
295 | SegmentLen := Length(Segment);
296 |
297 | if J <> paramsNode.ChildCount - 1 then
298 | Rec.Text := Rec.Text + ', ';
299 |
300 | node := node.NextBrother;
301 | end;
302 |
303 | AppendString(Rec.Text, ')');
304 | end
305 | else
306 | Rec.Identifier := AppendString(Rec.Text, Identifier.Identifier);
307 |
308 | if ResultType <> '' then
309 | begin
310 | AppendString(Rec.Text, ': ');
311 | Rec.ResultType := AppendString(Rec.Text, ResultType);
312 | end;
313 |
314 | Rec.Desc := Identifier.Node.DescAsString;
315 | if Identifier.Node <> nil then
316 | begin
317 | // for ctnTypeDefinition we need check first children
318 | if Identifier.Node.Desc = ctnTypeDefinition then
319 | begin
320 | childNode := Identifier.Node.FirstChild;
321 | if childNode <> nil then
322 | begin
323 | //if first children is ctnIdentifier
324 | if childNode.Desc = ctnIdentifier then
325 | begin
326 | //TODO: I think here should be search identifier and get it type
327 | Rec.IdentifierType := ctnNone;
328 | end
329 | else
330 | Rec.IdentifierType := childNode.Desc;
331 | end;
332 | end
333 | else
334 | Rec.IdentifierType := Identifier.Node.Desc
335 | end
336 | else
337 | Rec.IdentifierType := ctnUser;
338 |
339 |
340 | Callback(Rec, Writer);
341 | end;
342 | end;
343 | finally
344 | FreeAndNil(UniqueCheckStringList);
345 | end;
346 | end;
347 |
348 | type
349 | TCompletionRequest = record
350 | X, Y: Integer;
351 | FileName: String;
352 | TriggerKind: Integer;
353 | TriggerChar: string;
354 | IsRetrigger: Boolean;
355 | end;
356 |
357 | function ParseCompletionRequest(Reader: TJsonReader): TCompletionRequest;
358 | var
359 | Key: string;
360 | UriStr: string;
361 | begin
362 | UriStr := '';
363 | Result.TriggerKind := -1;
364 | Result.Y := -1;
365 | Result.X := -1;
366 |
367 | if Reader.Dict then
368 | while (Reader.Advance <> jsDictEnd) and Reader.Key(Key) do
369 | begin
370 | if (Key = 'textDocument') and Reader.Dict then
371 | while (Reader.Advance <> jsDictEnd) and Reader.Key(Key) do
372 | begin
373 | if Key = 'uri' then
374 | Reader.Str(UriStr);
375 | end
376 | else if (Key = 'position') and Reader.Dict then
377 | while (Reader.Advance <> jsDictEnd) and Reader.Key(Key) do
378 | begin
379 | if Key = 'line' then
380 | Reader.Number(Result.Y)
381 | else if (Key = 'character') then
382 | Reader.Number(Result.X);
383 | end
384 | else if (Key = 'context') and Reader.Dict then
385 | while (Reader.Advance <> jsDictEnd) and Reader.Key(Key) do
386 | begin
387 | if Key = 'triggerKind' then
388 | Reader.Number(Result.TriggerKind)
389 | else if Key = 'triggerCharacter' then
390 | Reader.Str(Result.TriggerChar)
391 | else if Key = 'isRetrigger' then
392 | Reader.Bool(Result.IsRetrigger);
393 | //else if Key = 'activeSignatureHelp' then
394 |
395 | end;
396 | end;
397 |
398 | Result.FileName := URIToFileNameEasy(UriStr);
399 | end;
400 |
401 | // Identifier completion
402 | procedure CompletionCallback(const Rec: TCompletionRec; Writer: TJsonWriter);
403 |
404 | // https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#completionItemKind
405 | procedure AddIdentifierKind(const Rec: TCompletionRec; Writer: TJsonWriter);
406 | begin
407 | DebugLog(Rec.Text + ' = ' + IntToStr(Rec.IdentifierType));
408 | case Rec.IdentifierType of
409 | ctnNone:
410 | Exit;
411 |
412 | ctnProcedure, ctnProcedureHead:
413 | begin
414 | Writer.Key('kind');
415 | Writer.Number(2); // method
416 | end;
417 |
418 | ctnBeginBlock, ctnSpecialize, ctnFinalization, ctnUser, ctnUnit, ctnInterface:
419 | begin
420 | Writer.Key('kind');
421 | Writer.Number(14); // keyword
422 | end;
423 |
424 | ctnClass:
425 | begin
426 | Writer.Key('kind');
427 | Writer.Number(7); //class
428 | end;
429 |
430 | ctnEnumerationType:
431 | begin
432 | Writer.Key('kind');
433 | Writer.Number(13); //enum
434 | end;
435 |
436 | ctnRangedArrayType:
437 | begin
438 | Writer.Key('kind');
439 | Writer.Number(12); // value? - I do not know what to choose
440 | end;
441 |
442 | ctnConstDefinition:
443 | begin
444 | Writer.Key('kind');
445 | Writer.Number(21); // constant
446 | end;
447 |
448 | ctnVarDefinition:
449 | begin
450 | Writer.Key('kind');
451 | Writer.Number(6); // variable
452 | end;
453 |
454 | ctnEnumIdentifier:
455 | begin
456 | Writer.Key('kind');
457 | Writer.Number(20); // enum member
458 | end;
459 |
460 | ctnUseUnitClearName:
461 | begin
462 | Writer.Key('kind');
463 | Writer.Number(9); // module
464 | end;
465 |
466 | ctnGlobalProperty, ctnProperty:
467 | begin
468 | Writer.Key('kind');
469 | Writer.Number(10); // property
470 | end;
471 |
472 | ctnTypeType, ctnTypeHelper:
473 | begin
474 | Writer.Key('kind');
475 | Writer.Number(25); // type?
476 | end;
477 | end;
478 | end;
479 |
480 | begin
481 | case IdentifierCodeCompletionStyle of
482 | ccsShowIdentifierWithParametersAndOverloads:
483 | begin
484 | // old style but fixed filtertext
485 |
486 | Writer.Dict;
487 | Writer.Key('insertText');
488 | Writer.Str(
489 | Copy(Rec.Text, Rec.Identifier.a, Rec.Identifier.b - Rec.Identifier.a)
490 | );
491 |
492 | Writer.Key('insertTextFormat');
493 | Writer.Number(1); // 1 = Plain Text
494 |
495 | Writer.Key('label');
496 | Writer.Str(Rec.Text);
497 |
498 | // text used to filter completion hint when we type
499 | Writer.Key('filterText');
500 | Writer.Str(
501 | Copy(Rec.Text, Rec.Identifier.a, Rec.Identifier.b - Rec.Identifier.a)
502 | );
503 |
504 | AddIdentifierKind(Rec, Writer);
505 |
506 | Writer.Key('detail');
507 | Writer.Str(Rec.Desc);
508 | Writer.DictEnd;
509 | end;
510 | ccsShowOnlyUniqueIdentifier:
511 | begin
512 | Writer.Dict;
513 | Writer.Key('insertText');
514 | Writer.Str(
515 | Copy(Rec.Text, Rec.Identifier.a, Rec.Identifier.b - Rec.Identifier.a)
516 | );
517 |
518 | Writer.Key('insertTextFormat');
519 | Writer.Number(1); // 1 = Plain Text
520 |
521 | Writer.Key('label');
522 | Writer.Str(
523 | Copy(Rec.Text, Rec.Identifier.a, Rec.Identifier.b - Rec.Identifier.a)
524 | );
525 |
526 | // text used to filter completion hint when we type
527 | Writer.Key('filterText');
528 | Writer.Str(
529 | Copy(Rec.Text, Rec.Identifier.a, Rec.Identifier.b - Rec.Identifier.a)
530 | );
531 |
532 | AddIdentifierKind(Rec, Writer);
533 |
534 | { https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#completionItemLabelDetails }
535 | {Writer.Key('labelDetails');
536 | Writer.Dict;
537 | Writer.Key('detail');
538 | Writer.Str(Rec.Text);
539 |
540 | Writer.Key('description');
541 | Writer.Str(Rec.Desc);
542 | Writer.DictEnd;}
543 |
544 | Writer.Key('detail');
545 | Writer.Str(Rec.Text);
546 |
547 | Writer.Key('documentation');
548 | Writer.Str(Rec.Desc);
549 |
550 | Writer.DictEnd;
551 | end;
552 | end;
553 | end;
554 |
555 | function GetPrefix(Code: TCodeBuffer; X, Y: integer): string;
556 | var
557 | PStart, PEnd: integer;
558 | Line: String;
559 | begin
560 | Line := Code.GetLine(Y);
561 | GetIdentStartEndAtPosition(Line, X + 1, PStart, PEnd);
562 | Result := Copy(Line, PStart, PEnd - PStart);
563 | end;
564 |
565 | { Send a notification using LSP "window/showMessage".
566 | Internally it will create and destroy a necessary TRpcResponse instance.
567 | Remember that sending "window/showMessage" is *not* a response to LSP request for completions,
568 | so you still need to send something else as completion response. }
569 | procedure ShowErrorMessage(const Rpc: TRpcPeer; const ErrorMessage: String);
570 | var
571 | Writer: TJsonWriter;
572 | MessageNotification: TRpcResponse;
573 | begin
574 | MessageNotification := TRpcResponse.CreateNotification('window/showMessage');
575 | try
576 | Writer := MessageNotification.Writer;
577 |
578 | Writer.Key('params');
579 | Writer.Dict;
580 | Writer.Key('type');
581 | Writer.Number(1); // type = 1 means "error"
582 | Writer.Key('message');
583 | Writer.Str(ErrorMessage);
584 | Writer.DictEnd;
585 |
586 | Rpc.Send(MessageNotification);
587 | finally
588 | FreeAndNil(MessageNotification);
589 | end;
590 | end;
591 |
592 | procedure TextDocument_Completion(Rpc: TRpcPeer; Request: TRpcRequest);
593 |
594 | { Create TRpcResponse with fake completion item, just to show ErrorMessage to user. }
595 | function CreateResponseFakeCompletionItem(const ErrorMessage: String): TRpcResponse;
596 | var
597 | Writer: TJsonWriter;
598 | begin
599 | Result := TRpcResponse.Create(Request.Id);
600 | Writer := Result.Writer;
601 |
602 | Writer.Dict;
603 | { Note that isIncomplete value is required.
604 | See spec: https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#completionList
605 | Emacs actually throws Lisp errors when it is missing. }
606 | Writer.Key('isIncomplete');
607 | Writer.Bool(false);
608 |
609 | Writer.Key('items');
610 | Writer.List;
611 |
612 | // Unfortunately, there isn't really a good way to report errors to the
613 | // client. While there are error responses, those aren't shown to the
614 | // user. There is also the call window/showMessage, but this one is not
615 | // implemented by NeoVim. So we work around it by showing a fake
616 | // completion item.
617 | Writer.Dict;
618 | Writer.Key('label');
619 | Writer.Str(ErrorMessage);
620 | Writer.Key('insertText');
621 | Writer.Str('');
622 | Writer.DictEnd;
623 |
624 | Writer.ListEnd;
625 |
626 | //Writer.Key('activeParameter');
627 | //Writer.Key('activeSignature');
628 | Writer.DictEnd;
629 | end;
630 |
631 | { Create TRpcResponse with no completions. }
632 | function CreateResponseNoCompletions: TRpcResponse;
633 | var
634 | Writer: TJsonWriter;
635 | begin
636 | Result := TRpcResponse.Create(Request.Id);
637 | Writer := Result.Writer;
638 |
639 | Writer.Dict;
640 | Writer.Key('isIncomplete');
641 | Writer.Bool(false); // the list is complete, we will not return more completions if you continue typing
642 |
643 | Writer.Key('items');
644 | Writer.List;
645 | Writer.ListEnd;
646 | Writer.DictEnd;
647 | end;
648 |
649 | var
650 | Req: TCompletionRequest;
651 | Code: TCodeBuffer;
652 | Prefix: string;
653 | Response: TRpcResponse;
654 | Writer: TJsonWriter;
655 | begin
656 | Response := nil;
657 | try
658 | try
659 | Req := ParseCompletionRequest(Request.Reader);
660 | Code := CodeToolBoss.FindFile(Req.FileName);
661 |
662 | if Code = nil then
663 | raise ERpcError.CreateFmt(
664 | jsrpcInvalidRequest,
665 | 'File not found: %s', [Req.FileName]
666 | );
667 |
668 | Prefix := GetPrefix(Code, Req.X, Req.Y);
669 | DebugLog('Complete: %d, %d, "%s"', [Req.X, Req.Y, Prefix]);
670 |
671 | Response := TRpcResponse.Create(Request.Id);
672 | Writer := Response.Writer;
673 |
674 | Writer.Dict;
675 | Writer.Key('isIncomplete');
676 | Writer.Bool(false);
677 |
678 | Writer.Key('items');
679 | Writer.List;
680 | case IdentifierCodeCompletionStyle of
681 | ccsShowIdentifierWithParametersAndOverloads:
682 | GetCompletionRecords(
683 | Code, Req.X + 1, Req.Y + 1, Prefix, false, true, false,
684 | @CompletionCallback, Writer
685 | );
686 | ccsShowOnlyUniqueIdentifier:
687 | GetCompletionRecords(
688 | Code, Req.X + 1, Req.Y + 1, Prefix, false, true, true,
689 | @CompletionCallback, Writer
690 | );
691 | end;
692 | Writer.ListEnd;
693 | Writer.DictEnd;
694 |
695 | Rpc.Send(Response);
696 | except
697 | on E: ERpcError do
698 | begin
699 | FreeAndNil(Response);
700 |
701 | case SyntaxErrorReportingMode of
702 | sermFakeCompletionItem:
703 | Response := CreateResponseFakeCompletionItem(E.Message);
704 | sermShowMessage:
705 | begin
706 | Response := CreateResponseNoCompletions;
707 | ShowErrorMessage(Rpc, E.Message);
708 | end;
709 | sermErrorResponse:
710 | Response := TRpcResponse.CreateError(Request.Id, 0, E.Message);
711 | end;
712 | Rpc.Send(Response);
713 | end;
714 | end;
715 | finally
716 | FreeAndNil(Response);
717 | end;
718 | end;
719 |
720 | // Signature help
721 |
722 | procedure SignatureCallback(const Rec: TCompletionRec; Writer: TJsonWriter);
723 | var
724 | i: Integer;
725 | begin
726 | Writer.Dict;
727 | Writer.Key('label');
728 | Writer.Str(Rec.Text);
729 |
730 | Writer.Key('parameters');
731 | Writer.List;
732 | for i := low(Rec.Parameters) to high(Rec.Parameters) do
733 | begin
734 | Writer.Dict;
735 | Writer.Key('label');
736 | Writer.List;
737 | Writer.Number(Rec.Parameters[i].a);
738 | Writer.Number(Rec.Parameters[i].b);
739 | Writer.ListEnd;
740 | // Writer.Key('documentation');
741 | Writer.DictEnd;
742 | end;
743 | Writer.ListEnd;
744 |
745 | //Writer.Key('documentation');
746 | //Writer.Key('activeParameter');
747 | Writer.DictEnd;
748 | end;
749 |
750 | procedure TextDocument_SignatureHelp(Rpc: TRpcPeer; Request: TRpcRequest);
751 | var
752 | Code: TCodeBuffer;
753 | ProcName: string;
754 | Req: TCompletionRequest;
755 | Response: TRpcResponse;
756 | Writer: TJsonWriter;
757 |
758 | function GetProcName(Code: TCodeBuffer; var X, Y: Integer): string;
759 | var
760 | CodeContexts: TCodeContextInfo;
761 | ProcStart: Integer;
762 | begin
763 | Result := '';
764 |
765 | CodeToolBoss.FindCodeContext(Code, X + 1, Y + 1, CodeContexts);
766 |
767 | if not Assigned(CodeContexts) then
768 | raise ERpcError.Create(jsrpcRequestFailed, CodeToolBoss.ErrorMessage);
769 |
770 | ProcStart := CodeContexts.StartPos;
771 |
772 | (*
773 | Testcase:
774 | - edit castle-engine/src/transform/castletransform_physics.inc
775 | - in TCastleCollider.CustomSerialization...
776 | - write ReadWriteBoolean and then type opening parenthesis "("
777 |
778 | Without the safeguard below, we have an occasional crash after LSP request
779 | {"jsonrpc":"2.0","method":"textDocument/signatureHelp","params":{"textDocument":{"uri":"file:///home/michalis/sources/castle-engine/castle-engine/src/transform/castletransform_physics.inc"},"position":{"line":1597,"character":21}},"id":2}
780 |
781 | The request looks OK (file uri, line and column numbers are OK).
782 | Debugging, the Code.Source value is also OK, contains the correct file text.
783 | But the ProcStart has weirdly large value, way beyond the file size.
784 |
785 | With the fix below, it only results in warning:
786 | Warning: GetProcName impossible, ProcStart (586344) beyond Length(Code.Source) (122268)
787 | Otherwise LSP server could crash with range check error when doing
788 | Code.Source[ProcStart] later.
789 | *)
790 | if ProcStart > Length(Code.Source) then
791 | begin
792 | DebugLog('Warning: GetProcName impossible, ProcStart (%d) beyond Length(Code.Source) (%d)', [
793 | ProcStart,
794 | Length(Code.Source)
795 | ]);
796 | Exit('');
797 | end;
798 |
799 | // Find closest opening parenthesis
800 | while (ProcStart > 1) and (Code.Source[ProcStart] <> '(') do
801 | Dec(ProcStart);
802 |
803 | // ProcStart point to the parenthesis before the first parameter.
804 | // But we actually need a position *inside* the procedure identifier.
805 | // Note that there may be whitespace, even newlines, between the first
806 | // parenthesis and the procedure.
807 | while (ProcStart > 1) and
808 | (Code.Source[ProcStart] in ['(', ' ', #13, #10, #9]) do
809 | Dec(ProcStart);
810 |
811 | Code.AbsoluteToLineCol(ProcStart, Y, X);
812 |
813 | Result := CodeContexts.ProcName;
814 | end;
815 | begin
816 | Response := nil;
817 | try
818 | try
819 | Req := ParseCompletionRequest(Request.Reader);
820 | Code := CodeToolBoss.FindFile(Req.FileName);
821 |
822 | if Code = nil then
823 | raise ERpcError.CreateFmt(
824 | jsrpcInvalidRequest,
825 | 'File not found: %s', [Req.FileName]
826 | );
827 |
828 | ProcName := GetProcName(Code, Req.X, Req.Y);
829 |
830 | Response := TRpcResponse.Create(Request.Id);
831 | Writer := Response.Writer;
832 |
833 | Writer.Dict;
834 | Writer.Key('signatures');
835 | Writer.List;
836 | GetCompletionRecords(
837 | Code, Req.X, Req.Y, ProcName, true, false, false, @SignatureCallback, Writer
838 | );
839 | Writer.ListEnd;
840 |
841 | //Writer.Key('activeParameter');
842 | //Writer.Key('activeSignature');
843 | Writer.DictEnd;
844 |
845 | Rpc.Send(Response);
846 | except
847 | on E: ERpcError do
848 | begin
849 | // Unfortunately, there isn't really a good way to report errors to the
850 | // client. While there are error responses, those aren't shown to the
851 | // user. There is also the call window/showMessage, but this one is not
852 | // implemented by NeoVim. So we work around it by showing a fake
853 | // completion item.
854 | FreeAndNil(Response);
855 | Response := TRpcResponse.Create(Request.Id);
856 | Writer := Response.Writer;
857 | Writer.Dict;
858 | Writer.Key('signatures');
859 | Writer.List;
860 | Writer.Dict;
861 | Writer.key('label');
862 | Writer.Str(e.Message);
863 | Writer.DictEnd;
864 | Writer.ListEnd;
865 |
866 | //Writer.Key('activeParameter');
867 | //Writer.Key('activeSignature');
868 | Writer.DictEnd;
869 | Rpc.Send(Response);
870 | end;
871 | end;
872 | finally
873 | FreeAndNil(Response);
874 | end;
875 | end;
876 |
877 | // Go to declaration
878 |
879 | type
880 | TJumpTarget = (jmpDeclaration, jmpDefinition);
881 |
882 | procedure TextDocument_JumpTo(
883 | Rpc: TRpcPeer; Request: TRpcRequest; Target: TJumpTarget
884 | );
885 | var
886 | Req: TCompletionRequest;
887 | Response: TRpcResponse;
888 | Writer: TJsonWriter;
889 |
890 | Code: TCodeBuffer;
891 | CurPos: TCodeXYPosition;
892 | NewPos: TCodeXYPosition;
893 |
894 | // Find declaration
895 | FoundDeclaration: Boolean;
896 | ExprType: TExpressionType;
897 |
898 | // Determine type
899 | IsProc: Boolean;
900 | CleanPos: Integer;
901 | Tool: TCodeTool;
902 | Node: TCodeTreeNode;
903 |
904 | // JumpToMethod
905 | FoundMethod: Boolean;
906 | NewTopLine,
907 | BlockTopLine,
908 | BlockBottomLine: Integer;
909 | RevertableJump: Boolean;
910 |
911 | Success: Boolean;
912 |
913 | begin
914 | Response := nil;
915 | Success := false;
916 | IsProc := false;
917 | Node := nil;
918 |
919 | try
920 | Req := ParseCompletionRequest(Request.Reader);
921 |
922 | Code := CodeToolBoss.FindFile(Req.FileName);
923 |
924 | if Code = nil then
925 | raise ERpcError.CreateFmt(
926 | jsrpcInvalidRequest,
927 | 'File not found: %s', [Req.FileName]
928 | );
929 |
930 | if not CodeToolBoss.InitCurCodeTool(Code) then
931 | raise ERpcError.CreateFmt(
932 | jsrpcRequestFailed,
933 | 'Could not initialize code tool', []
934 | );
935 |
936 | CurPos.Code := Code;
937 | CurPos.X := Req.X + 1;
938 | CurPos.Y := Req.Y + 1;
939 |
940 | DebugLog(
941 | 'Find declaration/definition: %d, %d "%s"',
942 | [Req.X, Req.Y, GetPrefix(Code, Req.X, Req.Y)]
943 | );
944 |
945 | try
946 | // Find declaration
947 | FoundDeclaration :=
948 | (Target in [jmpDeclaration, jmpDefinition]) and
949 | CodeToolBoss.CurCodeTool.FindDeclaration(
950 | CurPos, DefaultFindSmartHintFlags+[fsfSearchSourceName],
951 | ExprType, NewPos, NewTopLine
952 |
953 | );
954 | if FoundDeclaration then
955 | begin
956 | CurPos := NewPos;
957 | Success := true;
958 |
959 | // Determine type
960 | if CodeToolBoss.InitCurCodeTool(CurPos.Code) then
961 | begin
962 | Tool := CodeToolBoss.CurCodeTool;
963 | assert(Assigned(Tool));
964 | if Tool.CaretToCleanPos(CurPos, CleanPos) = 0 then
965 | Node := Tool.FindDeepestNodeAtPos(CleanPos, false);
966 | if Assigned(Node) then
967 | IsProc := Node.Desc in [ctnProcedure, ctnProcedureHead];
968 | end;
969 | end;
970 |
971 | // Try to jump to method implementation
972 | FoundMethod :=
973 | FoundDeclaration and IsProc and (Target = jmpDefinition) and
974 | CodeToolBoss.JumpToMethod(
975 | CurPos.Code, CurPos.X, CurPos.Y, NewPos.Code, NewPos.X, NewPos.Y,
976 | NewTopline, BlockTopLine, BlockBottomLine, RevertableJump
977 | );
978 |
979 | if FoundMethod then
980 | begin
981 | CurPos := NewPos;
982 | Success := true;
983 | end;
984 | except
985 | on E: ECodeToolError do
986 | begin
987 | // exeption raised when we search identifier in some comments words
988 | // has id 20170421200105 so we then do not show message window in vscode
989 | if E.Id <> 20170421200105 then
990 | begin
991 | ShowErrorMessage(Rpc, PositionForErrorPrefix(E) + E.Message);
992 | end;
993 | end;
994 | { ELinkScannerError is raised from FindDeclaration e.g. when include file is missing.
995 | Without capturing it here, trying to jump to declarations when there's an error
996 | would crash pasls server. }
997 | on E: ELinkScannerError do
998 | ShowErrorMessage(Rpc, E.Message);
999 | end;
1000 |
1001 | Response := TRpcResponse.Create(Request.Id);
1002 | Writer := Response.Writer;
1003 |
1004 | (*It is possible to get here Sucess and CurPos.Code = nil.
1005 |
1006 | Testcase: ctrl + click on TFloatRectangle.Empty in comment like this:
1007 |
1008 | { Image region to which we should limit the display.
1009 | Empty (following @link(TFloatRectangle.Empty)) means using the whole image.
1010 |
1011 | Logging shows:
1012 |
1013 | {"jsonrpc":"2.0","id":9,"method":"textDocument/definition","params":{"textDocument":{"uri":"file:///home/michalis/sources/castle-engine/castle-engine/src/base_rendering/castleglimages_persistentimage.inc"},"position":{"line":230,"character":46}}}
1014 | Find declaration/definition: 46, 230 "write"
1015 | TextDocument_JumpTo debug: CurPos.Code<>nil False
1016 | FATAL EXCEPTION: Access violation
1017 |
1018 | $0000000000492077 TEXTDOCUMENT_JUMPTO, line 780 of utextdocument.pas
1019 | $0000000000492320 TEXTDOCUMENT_DEFINITION, line 825 of utextdocument.pas
1020 | $000000000040169A DISPATCH, line 64 of pasls.lpr
1021 | $00000000004017DA MAIN, line 90 of pasls.lpr
1022 | $00000000004022AC main, line 239 of pasls.lpr
1023 |
1024 | TODO: debug this to the end, why Code can be nil?
1025 | Quickly looking at how it is used above, and NewPos and how CodeTools
1026 | set it -- I don't see how it can.
1027 | *)
1028 | if Success and (CurPos.Code = nil) then
1029 | Success := false;
1030 |
1031 | if Success then
1032 | begin
1033 | Writer.Dict;
1034 | Writer.Key('uri');
1035 | Writer.Str(FileNameToURI(CurPos.Code.Filename));
1036 |
1037 | Writer.Key('range');
1038 | Writer.Dict;
1039 | Writer.Key('start');
1040 | Writer.Dict;
1041 | Writer.Key('line');
1042 | Writer.Number(CurPos.Y - 1);
1043 |
1044 | Writer.Key('character');
1045 | Writer.Number(CurPos.X - 1);
1046 | Writer.DictEnd;
1047 |
1048 | Writer.Key('end');
1049 | Writer.Dict;
1050 | Writer.Key('line');
1051 | Writer.Number(CurPos.Y - 1);
1052 |
1053 | Writer.Key('character');
1054 | Writer.Number(CurPos.X - 1);
1055 | Writer.DictEnd;
1056 | Writer.DictEnd;
1057 | Writer.DictEnd;
1058 | end
1059 | else
1060 | begin
1061 | Writer.Null;
1062 | end;
1063 |
1064 | Rpc.Send(Response);
1065 | finally
1066 | FreeAndNil(Response);
1067 | end;
1068 | end;
1069 |
1070 | procedure TextDocument_Declaration(Rpc: TRpcPeer; Request: TRpcRequest);
1071 | begin
1072 | TextDocument_JumpTo(Rpc, Request, jmpDeclaration);
1073 | end;
1074 |
1075 | procedure TextDocument_Definition(Rpc: TRpcPeer; Request: TRpcRequest);
1076 | begin
1077 | TextDocument_JumpTo(Rpc, Request, jmpDefinition);
1078 | end;
1079 |
1080 | end.
1081 |
1082 |
--------------------------------------------------------------------------------
/server/uutils.pas:
--------------------------------------------------------------------------------
1 | // Pascal Language Server
2 | // Copyright 2021 Philip Zander
3 |
4 | // This file is part of Pascal Language Server.
5 |
6 | // Pascal Language Server is free software: you can redistribute it
7 | // and/or modify it under the terms of the GNU General Public License
8 | // as published by the Free Software Foundation, either version 3 of
9 | // the License, or (at your option) any later version.
10 |
11 | // Pascal Language Server is distributed in the hope that it will be
12 | // useful, but WITHOUT ANY WARRANTY; without even the implied warranty
13 | // of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 | // GNU General Public License for more details.
15 |
16 | // You should have received a copy of the GNU General Public License
17 | // along with Pascal Language Server. If not, see
18 | // .
19 |
20 | unit uutils;
21 |
22 | {$mode objfpc}{$H+}
23 |
24 | interface
25 |
26 | uses CustomCodeTool, CodeToolManager, CodeCache;
27 |
28 | function MergePaths(Paths: array of string): string;
29 | function GetConfigDirForApp(AppName, Vendor: string; Global: Boolean): string;
30 | function URIToFileNameEasy(const UriStr: String): String;
31 |
32 | { Return prefix for error message describing filename, line, column
33 | from ECodeToolError, if any. }
34 | function PositionForErrorPrefix(const E: ECodeToolError): String; overload;
35 |
36 | { Return prefix for error message describing filename, line, column
37 | from TCodeToolManager, if any. }
38 | function PositionForErrorPrefix(const CodeToolBoss: TCodeToolManager): String; overload;
39 |
40 | implementation
41 |
42 | uses
43 | SysUtils, URIParser,
44 | ujsonrpc;
45 |
46 | function MergePaths(Paths: array of string): string;
47 | var
48 | i: Integer;
49 | begin
50 | Result := '';
51 | for i := low(Paths) to high(Paths) do
52 | begin
53 | if (Result <> '') and (Paths[i] <> '') then
54 | Result := Result + ';' + Paths[i]
55 | else if (Result = '') and (Paths[i] <> '') then
56 | Result := Paths[i];
57 | end;
58 | end;
59 |
60 |
61 | // yuck
62 | var
63 | _FakeAppName, _FakeVendorName: string;
64 |
65 | function GetFakeAppName: string;
66 | begin
67 | Result := _FakeAppName;
68 | end;
69 |
70 | function GetFakeVendorName: string;
71 | begin
72 | Result := _FakeVendorName;
73 | end;
74 |
75 | function GetConfigDirForApp(AppName, Vendor: string; Global: Boolean): string;
76 | var
77 | OldGetAppName: TGetAppNameEvent;
78 | OldGetVendorName: TGetVendorNameEvent;
79 | begin
80 | _FakeAppName := AppName;
81 | _FakeVendorName := Vendor;
82 | OldGetAppName := OnGetApplicationName;
83 | OldGetVendorName := OnGetVendorName;
84 | try
85 | OnGetApplicationName := @GetFakeAppName;
86 | OnGetVendorName := @GetFakeVendorName;
87 | Result := GetAppConfigDir(Global);
88 | finally
89 | OnGetApplicationName := OldGetAppName;
90 | OnGetVendorName := OldGetVendorName;
91 | end;
92 | end;
93 |
94 | { Convert URI (with file:// protocol) to a filename.
95 | Accepts also empty string, returning empty string in return.
96 | Other / invalid URIs result in an exception. }
97 | function URIToFileNameEasy(const UriStr: String): String;
98 | begin
99 | if UriStr = '' then
100 | Exit('');
101 | if not URIToFilename(UriStr, Result) then
102 | raise ERpcError.CreateFmt(
103 | jsrpcInvalidRequest,
104 | 'Unable to convert URI to filename: %s', [UriStr]
105 | );
106 | end;
107 |
108 | const
109 | { Error prefix to display filename (may be ''), line, column.
110 | Note: line endings (#10, #13 or both) are ignored inside this, at least by VS Code.
111 | And \r \n are not interpreted as line endings, at least by VS Code.
112 | So we cannot make a newline break here. }
113 | SErrorPrefix = '%s(%d,%d): ';
114 |
115 | { Return prefix for error message describing position (line, column)
116 | from ECodeToolError, if any. }
117 | function PositionForErrorPrefix(const E: ECodeToolError): String;
118 |
119 | function PosSet(const Pos: TCodeXYPosition): Boolean;
120 | begin
121 | Result := (Pos.X <> 0) and (Pos.Y <> 0);
122 | end;
123 |
124 | function PosToStr(const Pos: TCodeXYPosition): String;
125 | var
126 | CodeFileName: String;
127 | begin
128 | if Pos.Code <> nil then
129 | CodeFileName := ExtractFileName(Pos.Code.Filename)
130 | else
131 | CodeFileName := '';
132 | Result := Format(SErrorPrefix, [CodeFileName, Pos.Y, Pos.X]);
133 | end;
134 |
135 | begin
136 | if E.Sender <> nil then
137 | begin
138 | if PosSet(E.Sender.ErrorNicePosition) then
139 | Exit(PosToStr(E.Sender.ErrorNicePosition));
140 | if PosSet(E.Sender.ErrorPosition) then
141 | Exit(PosToStr(E.Sender.ErrorPosition));
142 | end;
143 | Result := '';
144 | end;
145 |
146 | function PositionForErrorPrefix(const CodeToolBoss: TCodeToolManager): String;
147 | var
148 | CodeFileName: String;
149 | begin
150 | Result := '';
151 | if (CodeToolBoss.ErrorLine <> 0) and
152 | (CodeToolBoss.ErrorColumn <> 0) then
153 | begin
154 | if CodeToolBoss.ErrorCode <> nil then
155 | CodeFileName := ExtractFileName(CodeToolBoss.ErrorCode.Filename)
156 | else
157 | CodeFileName := '';
158 | Result := Format(SErrorPrefix, [
159 | CodeFileName,
160 | CodeToolBoss.ErrorLine,
161 | CodeToolBoss.ErrorColumn
162 | ]);
163 | end;
164 | end;
165 |
166 | end.
167 |
168 |
--------------------------------------------------------------------------------
/server/uworkspacesymbolsupport.pas:
--------------------------------------------------------------------------------
1 | unit UWorkspaceSymbolSupport;
2 |
3 | {
4 | Implementation of WorkspaceSymbol
5 |
6 | Docs: https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#workspace_symbol
7 | }
8 |
9 | {$mode ObjFPC}{$H+}
10 |
11 | interface
12 |
13 | uses
14 | Classes, SysUtils, jsonstream, ujsonrpc, uutils;
15 |
16 | type
17 | { Enumeration of symbol kinds based on
18 | https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#symbolKind }
19 | TSymbolKind = (
20 | skFile = 1,
21 | skModule,
22 | skNamespace,
23 | skPackage,
24 | skClass,
25 | skMethod,
26 | skProperty,
27 | skField,
28 | skConstructor,
29 | skEnum,
30 | skInterface,
31 | skFunction,
32 | skVariable,
33 | skConstant,
34 | skString,
35 | skNumber,
36 | skBoolean,
37 | skArray,
38 | skObject,
39 | skKey,
40 | skNull,
41 | skEnumMember,
42 | skStruct,
43 | skEvent,
44 | skOperator,
45 | skTypeOperator
46 | );
47 |
48 | TSymbolTag = (
49 | stDeprecated
50 | );
51 |
52 | TSymbolTags = set of TSymbolTag;
53 |
54 | procedure WorkspaceSymbol(Rpc: TRpcPeer; Request: TRpcRequest; const Directories: TStrings);
55 |
56 | implementation
57 |
58 | uses ulogvscode, CodeToolManager, CodeCache, CodeTree, URIParser, PascalParserTool;
59 |
60 | function ParseDocumentSymbolRequest(Reader: TJsonReader): String;
61 | var
62 | Key, Uri: String;
63 | begin
64 | Uri := '';
65 | if Reader.Dict then
66 | while (Reader.Advance <> jsDictEnd) and Reader.Key(Key) do
67 | begin
68 | if Key = 'query' then
69 | begin
70 | Reader.Str(Uri);
71 | break;
72 | end;
73 | end;
74 | Result := URIToFileNameEasy(Uri);
75 | end;
76 |
77 | { Responses for textDocument/documentSymbol method
78 | Docs: https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#workspace_symbol }
79 | procedure WorkspaceSymbol(Rpc: TRpcPeer; Request: TRpcRequest; const Directories: TStrings);
80 | var
81 | FileName, Query: String;
82 | Code: TCodeBuffer;
83 | CodeTool: TCodeTool;
84 | CodeTreeNode: TCodeTreeNode;
85 |
86 | Node: TCodeTreeNode;
87 |
88 | Response: TRpcResponse;
89 | Writer: TJsonWriter;
90 |
91 | StartCaret: TCodeXYPosition;
92 | EndCaret: TCodeXYPosition;
93 | ProcedureName: String;
94 |
95 | Files: TStrings;
96 | FilesWithPaths: TStringList;
97 | I, J: Integer;
98 | Directory: String;
99 | begin
100 | Query := ParseDocumentSymbolRequest(Request.Reader);
101 | LogInfo(Rpc, 'Query:' + Query);
102 |
103 | FilesWithPaths := TStringList.Create;
104 | try
105 | Files := TStringList.Create;
106 | try
107 | for I := 0 to Directories.Count - 1 do
108 | begin
109 | Directory := IncludeTrailingPathDelimiter(Directories[I]);
110 | //LogInfo(Rpc, 'Directory1:' + Directory);
111 | //LogInfo(Rpc, 'Directory2:' + Directories[I]);
112 | Files.Clear;
113 |
114 | CodeToolBoss.SourceCache.DirectoryCachePool.GetListing(Directory, Files, false);
115 |
116 | for J := 0 to Files.Count - 1 do
117 | begin
118 | FileName := Files[J];
119 | if LowerCase(ExtractFileExt(FileName)) <> '.pas' then
120 | continue;
121 | FilesWithPaths.Add(Directory + FileName);
122 | //LogInfo(Rpc, 'File:' + FileName);
123 | //LogInfo(Rpc, 'FileWithPaths:' + Directory + FileName);
124 | end;
125 | end;
126 | finally
127 | FreeAndNil(Files);
128 | end;
129 |
130 | Response := nil;
131 | try
132 | Response := TRpcResponse.Create(Request.Id);
133 | Writer := Response.Writer;
134 |
135 | Writer.List;
136 | for I := 0 to FilesWithPaths.Count -1 do
137 | begin
138 | FileName := FilesWithPaths[I];
139 |
140 | Code := CodeToolBoss.FindFile(Filename);
141 |
142 | if Code = nil then
143 | begin
144 | Code := CodeToolBoss.LoadFile(FileName,false, false);
145 | if Code = nil then
146 | Continue;
147 | end;
148 |
149 | CodeToolBoss.Explore(Code, CodeTool, false, false);
150 |
151 | { This happens when opening include file without MainUnit,
152 | like https://github.com/castle-engine/castle-engine/blob/master/src/common_includes/castleconf.inc .
153 | Return empty response. }
154 | if CodeTool = nil then
155 | Continue;
156 |
157 | if CodeTool.Tree = nil then
158 | raise ERpcError.Create(jsrpcRequestFailed, 'Code tool tree is nil.');
159 |
160 | { This check fails when pas file is empty, return null in response }
161 | if CodeTool.Tree.Root = nil then
162 | Continue;
163 |
164 | CodeTreeNode := CodeTool.FindImplementationNode;
165 | { When there is no implementation section try to parse interface }
166 | if CodeTreeNode = nil then
167 | CodeTreeNode := CodeTool.FindInterfaceNode;
168 |
169 | if CodeTreeNode = nil then
170 | Continue;
171 |
172 | { Based on lazarus TProcedureListForm.AddToGrid() and other functions }
173 | Node := CodeTreeNode;
174 | while Node <> nil do
175 | begin
176 | // LogInfo(Rpc, 'Node: ' + Node.DescAsString);
177 | if Node.Desc = ctnProcedure then
178 | begin
179 | { LogInfo(Rpc, CodeTool.ExtractProcHead(Node, [phpAddParentProcs,
180 | phpWithoutParamList, phpWithoutBrackets, phpWithoutSemicolon])); }
181 |
182 | { Get the real position in source file }
183 |
184 | CodeTool.CleanPosToCaret(Node.StartPos, StartCaret);
185 | CodeTool.CleanPosToCaret(Node.EndPos, EndCaret);
186 |
187 | ProcedureName := CodeTool.ExtractProcHead(Node, [phpAddParentProcs,
188 | phpWithoutParamList, phpWithoutBrackets, phpWithoutSemicolon]);
189 |
190 | { Check procedure name is not empty, that makes vscode returns errors.
191 | Can happen when we start write new procedure. }
192 | if Trim(ProcedureName) = '' then
193 | begin
194 | Node := Node.Next;
195 | continue;
196 | end;
197 |
198 | Writer.Dict;
199 | Writer.Key('name');
200 | Writer.Str(ProcedureName);
201 |
202 | Writer.Key('kind');
203 | Writer.Number(Integer(skMethod));
204 |
205 | Writer.Key('location');
206 | Writer.Dict;
207 | Writer.Key('uri');
208 | Writer.Str(FilenameToURI(StartCaret.Code.Filename));
209 | Writer.Key('range');
210 | Writer.Dict;
211 | Writer.Key('start');
212 | Writer.Dict;
213 | Writer.Key('line');
214 | Writer.Number(StartCaret.Y - 1 );
215 | Writer.Key('character');
216 | Writer.Number(StartCaret.X);
217 | Writer.DictEnd;
218 | Writer.Key('end');
219 | Writer.Dict;
220 | Writer.Key('line');
221 | Writer.Number(EndCaret.Y - 1);
222 | Writer.Key('character');
223 | Writer.Number(EndCaret.X);
224 | Writer.DictEnd;
225 | Writer.DictEnd;
226 | Writer.DictEnd;
227 | Writer.DictEnd;
228 | end;
229 | Node := Node.Next;
230 | end;
231 |
232 | end;
233 | Writer.ListEnd;
234 | Rpc.Send(Response);
235 | finally
236 | FreeAndNil(Response);
237 | end;
238 |
239 | finally
240 | FreeAndNil(FilesWithPaths);
241 | end;
242 | end;
243 |
244 | end.
245 |
246 |
--------------------------------------------------------------------------------