1 module served.workers.rename_listener;
2 
3 import std.algorithm;
4 import std.array;
5 import std.datetime;
6 import std.experimental.logger;
7 import std.path;
8 import std..string;
9 
10 import served.extension;
11 import served.types;
12 import served.utils.translate;
13 
14 import workspaced.com.moduleman : ModulemanComponent;
15 
16 /// Helper struct which contains information about a recently opened and/or created file
17 struct FileOpenInfo
18 {
19 	/// When the file first was accessed recently
20 	SysTime at;
21 	/// The uri of the file
22 	DocumentUri uri;
23 	/// Whether the file has only been created, opened or both
24 	bool created, opened;
25 
26 	/// Returns: true when the FileOpenInfo should no longer be used (after 5 seconds)
27 	bool expired(SysTime now) const
28 	{
29 		return at == SysTime.init || (now - at) > 5.seconds;
30 	}
31 
32 	/// Sets created to true and triggers onFullyCreate if it is also opened
33 	void setCreated()
34 	{
35 		created = true;
36 		if (opened)
37 			onFullyCreate(uri);
38 	}
39 
40 	/// Sets opened to true and triggers onFullyCreate if it is also created
41 	void setOpened()
42 	{
43 		opened = true;
44 		if (created)
45 			onFullyCreate(uri);
46 	}
47 }
48 
49 /// Helper struct to keep track of recently opened & created files
50 struct RecentFiles
51 {
52 	/// the last 8 files
53 	FileOpenInfo[8] infos;
54 
55 	/// Returns a reference to some initialized FileOpenInfo with this uri
56 	ref FileOpenInfo get(DocumentUri uri) return
57 	{
58 		auto now = Clock.currTime;
59 
60 		// find existing one
61 		foreach (ref info; infos)
62 		{
63 			if (info.uri == uri)
64 			{
65 				if (info.expired(now))
66 				{
67 					info.created = false;
68 					info.opened = false;
69 				}
70 				return info;
71 			}
72 		}
73 
74 		// replace old one
75 		size_t min;
76 		SysTime minTime = now;
77 		foreach (i, ref info; infos)
78 		{
79 			if (info.at < minTime)
80 			{
81 				minTime = info.at;
82 				min = i;
83 			}
84 		}
85 
86 		infos[min].at = now;
87 		infos[min].created = infos[min].opened = false;
88 		infos[min].uri = uri;
89 		return infos[min];
90 	}
91 }
92 
93 package __gshared RecentFiles recentFiles;
94 
95 @protocolNotification("workspace/didChangeWatchedFiles")
96 void markRecentlyChangedFile(DidChangeWatchedFilesParams params)
97 {
98 	foreach (change; params.changes)
99 		if (change.type == FileChangeType.created)
100 			markRecentFileCreated(change.uri);
101 }
102 
103 void markRecentFileCreated(DocumentUri uri)
104 {
105 	recentFiles.get(uri).setCreated();
106 }
107 
108 @protocolNotification("textDocument/didOpen")
109 void markRecentFileOpened(DidOpenTextDocumentParams params)
110 {
111 	recentFiles.get(params.textDocument.uri).setOpened();
112 }
113 
114 /// Called when a file has been created or renamed and opened within a short time frame by the user
115 /// Indicating it was created to be edited in the IDE
116 void onFullyCreate(DocumentUri uri)
117 {
118 	trace("handle file creation/rename for ", uri);
119 	if (uri.endsWith(".d"))
120 		return onFullyCreateDSource(uri);
121 
122 	auto file = baseName(uri);
123 	if (file == "dub.json")
124 		onFullyCreateDubJson(uri);
125 	else if (file == "dub.sdl")
126 		onFullyCreateDubSdl(uri);
127 }
128 
129 void onFullyCreateDSource(DocumentUri uri)
130 {
131 	auto fileConfig = config(uri);
132 	if (!fileConfig.d.generateModuleNames)
133 		return;
134 
135 	string workspace = workspaceRootFor(uri);
136 	auto document = documents[uri];
137 	// Sending applyEdit so it is undoable
138 	auto patches = backend.get!ModulemanComponent(workspace)
139 		.normalizeModules(uri.uriToFile, document.rawText);
140 	if (patches.length)
141 	{
142 		WorkspaceEdit edit;
143 		edit.changes[uri] = patches.map!(a => TextEdit(TextRange(document.bytesToPosition(a.range[0]),
144 				document.bytesToPosition(a.range[1])), a.content)).array;
145 		rpc.sendMethod("workspace/applyEdit", ApplyWorkspaceEditParams(edit));
146 		rpc.window.showInformationMessage(translate!"d.served.moduleNameAutoUpdated");
147 	}
148 }
149 
150 void onFullyCreateDubJson(DocumentUri uri)
151 {
152 	auto document = documents[uri];
153 	if (document.rawText.strip.length == 0)
154 	{
155 		string packageName = determineDubPackageName(uri.uriToFile.dirName);
156 		WorkspaceEdit edit;
157 		edit.changes[uri] = [
158 			TextEdit(TextRange(0, 0, 0, 0), "{\n\t\"name\": \"" ~ packageName ~ "\"\n}")
159 		];
160 		rpc.sendMethod("workspace/applyEdit", ApplyWorkspaceEditParams(edit));
161 	}
162 }
163 
164 void onFullyCreateDubSdl(DocumentUri uri)
165 {
166 	auto document = documents[uri];
167 	if (document.rawText.strip.length == 0)
168 	{
169 		string packageName = determineDubPackageName(uri.uriToFile.dirName);
170 		WorkspaceEdit edit;
171 		edit.changes[uri] = [
172 			TextEdit(TextRange(0, 0, 0, 0), `name "` ~ packageName ~ `"` ~ '\n')
173 		];
174 		rpc.sendMethod("workspace/applyEdit", ApplyWorkspaceEditParams(edit));
175 	}
176 }
177 
178 /// Generates a package name for a given folder path
179 string determineDubPackageName(string directory)
180 {
181 	import std.ascii : toLower, isUpper, isAlphaNum;
182 
183 	auto name = baseName(directory);
184 	if (!name.length)
185 		return "";
186 
187 	auto ret = appender!string;
188 	ret.put(toLower(name[0]));
189 	bool wasUpper = name[0].isUpper;
190 	bool wasDash = false;
191 	foreach (char c; name[1 .. $])
192 	{
193 		if (!isAlphaNum(c) && c != '_')
194 			c = '-';
195 
196 		if (wasDash && c == '-')
197 			continue;
198 		wasDash = c == '-';
199 
200 		if (c.isUpper)
201 		{
202 			if (!wasUpper)
203 			{
204 				ret.put('-');
205 				ret.put(c.toLower);
206 			}
207 			wasUpper = true;
208 		}
209 		else
210 		{
211 			wasUpper = false;
212 			ret.put(c);
213 		}
214 	}
215 	auto packageName = ret.data;
216 	while (packageName.startsWith("-"))
217 		packageName = packageName[1 .. $];
218 	while (packageName.endsWith("-"))
219 		packageName = packageName[0 .. $ - 1];
220 	return packageName;
221 }