1 /*
2 *Copyright (C) 2018 Laurent Tréguier
3 *
4 *This file is part of DLS.
5 *
6 *DLS is free software: you can redistribute it and/or modify
7 *it under the terms of the GNU General Public License as published by
8 *the Free Software Foundation, either version 3 of the License, or
9 *(at your option) any later version.
10 *
11 *DLS is distributed in the hope that it will be useful,
12 *but WITHOUT ANY WARRANTY; without even the implied warranty of
13 *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 DLS. If not, see <http://www.gnu.org/licenses/>.
18 *
19 */
20
21 module dls.util.document;
22
23 class Document
24 {
25 import dls.util.uri : Uri;
26 import dls.protocol.definitions : DocumentUri, Position, Range,
27 TextDocumentIdentifier, TextDocumentItem, VersionedTextDocumentIdentifier;
28 import dls.protocol.interfaces : TextDocumentContentChangeEvent;
29 import std.json : JSONValue;
30
31 private static Document[string] _documents;
32 private DocumentUri _uri;
33 private wstring[] _lines;
34 private JSONValue _version;
35
36 @property static auto uris()
37 {
38 import std.algorithm : map;
39
40 return _documents.byValue.map!(doc => new Uri(doc._uri));
41 }
42
43 static Document get(const Uri uri)
44 {
45 import std.file : readText;
46
47 return uri.path in _documents ? _documents[uri.path] : new Document(uri, readText(uri.path));
48 }
49
50 static bool open(const TextDocumentItem textDocument)
51 {
52 auto uri = new Uri(textDocument.uri);
53
54 if (uri.path !in _documents)
55 {
56 _documents[uri.path] = new Document(uri, textDocument.text);
57 _documents[uri.path]._version = textDocument.version_;
58 return true;
59 }
60 else
61 {
62 return false;
63 }
64 }
65
66 static bool close(const TextDocumentIdentifier textDocument)
67 {
68 auto uri = new Uri(textDocument.uri);
69
70 if (uri.path in _documents)
71 {
72 _documents.remove(uri.path);
73 return true;
74 }
75 else
76 {
77 return false;
78 }
79 }
80
81 static bool change(const VersionedTextDocumentIdentifier textDocument,
82 TextDocumentContentChangeEvent[] events)
83 {
84 auto uri = new Uri(textDocument.uri);
85
86 if (uri.path in _documents)
87 {
88 _documents[uri.path].change(events);
89 _documents[uri.path]._version = textDocument.version_;
90 return true;
91 }
92 else
93 {
94 return false;
95 }
96 }
97
98 @property const(wstring[]) lines() const
99 {
100 return _lines;
101 }
102
103 @property JSONValue version_() const
104 {
105 return _version;
106 }
107
108 private this(const Uri uri, const string text)
109 {
110 _uri = uri;
111 _lines = getText(text);
112 }
113
114 override string toString() const
115 {
116 import std.range : join;
117 import std.utf : toUTF8;
118
119 return _lines.join().toUTF8();
120 }
121
122 void validatePosition(const Position position) const
123 {
124 import dls.protocol.errors : InvalidParamsException;
125 import std.format : format;
126
127 if (position.line >= _lines.length || position.character > _lines[position.line].length)
128 {
129 throw new InvalidParamsException(format!"invalid position: %s %s,%s"(_uri,
130 position.line, position.character));
131 }
132 }
133
134 size_t byteAtPosition(const Position position) const
135 {
136 import std.algorithm : min;
137 import std.utf : codeLength;
138
139 size_t result;
140
141 foreach (i, ref line; _lines)
142 {
143 if (i < position.line)
144 {
145 result += codeLength!char(line);
146 }
147 else
148 {
149 result += codeLength!char(line[0 .. min(position.character, $)]);
150 break;
151 }
152 }
153
154 return result;
155 }
156
157 Position positionAtByte(size_t bytePosition) const
158 {
159 import std.algorithm : min;
160 import std.utf : codeLength, toUTF8;
161
162 size_t i;
163 size_t bytes;
164
165 while (bytes <= bytePosition && i < _lines.length)
166 {
167 bytes += codeLength!char(_lines[i]);
168 ++i;
169 }
170
171 immutable lineNumber = minusOne(i);
172 immutable line = _lines[lineNumber];
173 bytes -= codeLength!char(line);
174 immutable columnByte = min(bytePosition - bytes, line.length);
175 immutable columnNumber = codeLength!wchar(line.toUTF8()[0 .. columnByte]);
176 return new Position(lineNumber, columnNumber);
177 }
178
179 Range wordRangeAtPosition(const Position position) const
180 {
181 import std.algorithm : min;
182
183 immutable line = _lines[min(position.line, $ - 1)];
184 immutable middleIndex = min(position.character, line.length);
185 size_t startIndex = middleIndex;
186 size_t endIndex = middleIndex;
187
188 static bool isIdentifierChar(wchar c)
189 {
190 import std.ascii : isPunctuation, isWhite;
191
192 return !isWhite(c) && (!isPunctuation(c) || c == '_');
193 }
194
195 while (startIndex > 0 && isIdentifierChar(line[minusOne(startIndex)]))
196 {
197 --startIndex;
198 }
199
200 while (endIndex < line.length && isIdentifierChar(line[endIndex]))
201 {
202 ++endIndex;
203 }
204
205 return new Range(new Position(position.line, startIndex),
206 new Position(position.line, endIndex));
207 }
208
209 Range wordRangeAtLineAndByte(size_t lineNumber, size_t bytePosition) const
210 {
211 import std.algorithm : min;
212 import std.utf : codeLength, toUTF8;
213
214 return wordRangeAtPosition(new Position(lineNumber,
215 codeLength!wchar(_lines[lineNumber].toUTF8()[0 .. min(bytePosition, $)])));
216 }
217
218 Range wordRangeAtByte(size_t bytePosition) const
219 {
220 return wordRangeAtPosition(positionAtByte(bytePosition));
221 }
222
223 private void change(const TextDocumentContentChangeEvent[] events)
224 {
225 foreach (event; events)
226 {
227 if (event.range.isNull)
228 {
229 _lines = getText(event.text);
230 }
231 else
232 {
233 with (event.range)
234 {
235 auto linesBefore = _lines[0 .. start.line];
236 auto linesAfter = _lines[end.line + 1 .. $];
237
238 auto lineStart = _lines[start.line][0 .. start.character];
239 auto lineEnd = _lines[end.line][end.character .. $];
240
241 auto newLines = getText(event.text);
242
243 if (newLines.length)
244 {
245 newLines[0] = lineStart ~ newLines[0];
246 newLines[$ - 1] = newLines[$ - 1] ~ lineEnd;
247 }
248 else
249 {
250 newLines = [lineStart ~ lineEnd];
251 }
252
253 _lines = linesBefore ~ newLines ~ linesAfter;
254 }
255 }
256 }
257 }
258
259 private wstring[] getText(const string text) const
260 {
261 import std.algorithm : endsWith;
262 import std.array : replaceFirst;
263 import std.encoding : getBOM;
264 import std..string : splitLines;
265 import std.typecons : Yes;
266 import std.utf : toUTF16;
267
268 auto lines = text.replaceFirst(cast(string) getBOM(cast(ubyte[]) text)
269 .sequence, "").toUTF16().splitLines(Yes.keepTerminator);
270
271 if (!lines.length || lines[$ - 1].endsWith('\r', '\n'))
272 {
273 lines ~= "";
274 }
275
276 return lines;
277 }
278 }
279
280 size_t minusOne(size_t i)
281 {
282 return i > 0 ? i - 1 : 0;
283 }